סקירה כללית של ViewModel   בארגז הכלים Android Jetpack.

כדאי לנסות בשילוב עם Kotlin Multiplatform
באמצעות Kotlin Multiplatform אפשר לשתף את הלוגיקה העסקית עם פלטפורמות אחרות. איך מגדירים את ViewModel ומשתמשים בו ב-KMP

המחלקות ViewModel הן מאחסני מצב ברמת המסך או ברמת הלוגיקה העסקית. הוא חושף את המצב לממשק המשתמש ומכיל לוגיקה עסקית קשורה. היתרון העיקרי שלו הוא שהוא שומר במטמון את המצב וממשיך אותו גם אחרי שינויים בהגדרות. המשמעות היא שממשק המשתמש לא צריך לאחזר נתונים מחדש כשעוברים בין פעילויות או אחרי שינויים בהגדרות, למשל כשמסובבים את המסך.

מידע נוסף על בעלי זכויות במקרקעין זמין במאמר בעלי זכויות במקרקעין. באופן דומה, מידע נוסף על שכבת ממשק המשתמש באופן כללי זמין במאמר שכבת ממשק המשתמש.

היתרונות של ViewModel

החלופה ל-ViewModel היא מחלקה רגילה שמכילה את הנתונים שמוצגים בממשק המשתמש. הבעיה הזו יכולה לקרות כשעוברים בין פעילויות או בין יעדי ניווט. אם לא מאחסנים את הנתונים באמצעות מנגנון שמירת מצב המופע, הם נמחקים. ‫ViewModel מספק API נוח להתמדת נתונים, שפותר את הבעיה הזו.

לחלופין, עבור רכיבים שמחזיקים מצב בלבד, Compose מציע יכולות של retain שמאפשרות למחלקות פשוטות לשרוד שינויים בהגדרות בלי התשתית המלאה של ViewModel. שני המנגנונים עוזרים לשמור על המצב, אבל בדרך כלל בטוח יותר לספק ViewModel למופע שנשמר מאשר להפך, כי מחזור החיים והתנהגויות הניקוי שלהם שונים.

יש שני יתרונות עיקריים לשימוש במחלקה ViewModel:

  • היא מאפשרת לכם לשמור את מצב ממשק המשתמש.
  • הוא מספק גישה ללוגיקה עסקית.

התמדה

‫ViewModel מאפשר שמירה של נתונים גם דרך המצב שמוחזק על ידי ViewModel וגם דרך הפעולות שמופעלות על ידי ViewModel. המשמעות של שמירת הנתונים במטמון היא שלא צריך לאחזר את הנתונים שוב באמצעות שינויים נפוצים בהגדרות, כמו סיבוב המסך.

היקף

כשיוצרים מופע של ViewModel, מעבירים לו אובייקט שמטמיע את הממשק ViewModelStoreOwner. יכול להיות שזה יעד ניווט, תרשים ניווט, פעילות או כל סוג אחר שמטמיע את הממשק. אפשר גם להגדיר את ה-ViewModel ישירות ל-composable באמצעות rememberViewModelStoreOwner API. ה-ViewModel מוגדר בהמשך למחזור החיים של ViewModelStoreOwner. הוא נשאר בזיכרון עד ש-ViewModelStoreOwnerנעלם באופן סופי (למשל, כשהבעלים של הרכיב שניתן להרכבה יוצא מהקומפוזיציה).

טווח של מחלקות הן מחלקות משנה ישירות או עקיפות של הממשק ViewModelStoreOwner. מחלקות המשנה הישירות הן ComponentActivity ו-NavBackStackEntry. רשימה מלאה של מחלקות משנה עקיפות זמינה בחומר העזר בנושא ViewModelStoreOwner. כדי להגדיר את ה-ViewModels לפריטים ספציפיים ב-LazyList או ב-Pager, משתמשים ב-rememberViewModelStoreProvider() כדי להעביר את ניהול הבעלים לרכיב האב.

כשפעילות המארח עוברת שינוי בהגדרות, עבודה אסינכרונית ממשיכה ב-ViewModel, בין אם היא מוגבלת לפעילות או לרכיב Composable ספציפי. זה המפתח להתמדה.

מידע נוסף זמין בקטע מחזור החיים של ViewModel שבהמשך, במאמר בנושא ממשקי API של ViewModel Scoping ובמדריך בנושא העלאת הרמה של מצב (state hoisting) ב-Jetpack פיתוח נייטיב.

SavedStateHandle

SavedStateHandle מאפשר לשמור נתונים לא רק במהלך שינויים בהגדרות, אלא גם במהלך השבתת תהליך. כלומר, הוא מאפשר לשמור את מצב ממשק המשתמש גם כשהמשתמש סוגר את האפליקציה ופותח אותה מאוחר יותר.

מידע נוסף על שמירת מצב ממשק המשתמש זמין במאמר שמירת מצב ממשק המשתמש בפיתוח נייטיב.

גישה ללוגיקה עסקית

למרות שרוב הלוגיקה העסקית נמצאת בשכבת הנתונים, יכול להיות ששכבת ממשק המשתמש תכיל גם לוגיקה עסקית. זה יכול לקרות כשמשלבים נתונים מכמה מאגרי מידע כדי ליצור את מצב ממשק המשתמש במסך, או כשסוג מסוים של נתונים לא דורש שכבת נתונים.

‫ViewModel הוא המקום הנכון לטפל בלוגיקה עסקית בשכבת ממשק המשתמש. ה-ViewModel אחראי גם לטיפול באירועים ולהעברתם לשכבות אחרות בהיררכיה כשצריך להחיל לוגיקה עסקית כדי לשנות נתונים באפליקציה.

הטמעה של ViewModel

הדוגמה הבאה היא הטמעה של ViewModel למסך שמאפשר למשתמש לגלגל קוביות.

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

אחר כך אפשר לגשת ל-ViewModel מרכיב Composable ברמת המסך באופן הבא:

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

שימוש בקורוטינות עם ViewModel

ViewModel כולל תמיכה בשגרות משנה של Kotlin. הוא יכול לשמור עבודה אסינכרונית באותו אופן שבו הוא שומר את מצב ממשק המשתמש.

מידע נוסף זמין במאמר שימוש בשגרות משנה (coroutines) ב-Kotlin עם רכיבי ארכיטקטורה של Android.

מחזור החיים של ViewModel

מחזור החיים של ViewModel קשור ישירות להיקף שלו. ViewModel נשאר בזיכרון עד שViewModelStoreOwner שאליו הוא משויך נעלם. מצב כזה יכול לקרות בהקשרים הבאים:

  • במקרה של פעילות, כשהיא מסתיימת.
  • במקרה של רשומה של ניווט, כשהיא מוסרת ממקבץ הפעולות הקודמות.
  • במקרה של רכיב קומפוזבילי, כשהוא יוצא מהקומפוזיציה. אפשר להשתמש ב-rememberViewModelStoreOwner כדי להגדיר ViewModel ישירות לחלק שרירותי בממשק המשתמש (כמו Pager או LazyList).

לכן, ViewModels הם פתרון מצוין לאחסון נתונים שנשמרים גם אחרי שינויים בהגדרות.

איור 1 ממחיש את מצבי מחזור החיים השונים של פעילות מסוימת במהלך סיבוב ואז בסיום. באיור מוצג גם משך החיים של ViewModel לצד מחזור החיים של הפעילות המשויכת. בתרשים הזה מוצגים המצבים של פעילות.

איור שמציג את מחזור החיים של ViewModel כשמצב הפעילות משתנה.
איור 1. מצבי מחזור החיים של פעילות ושל ViewModel.

בדרך כלל מבקשים ViewModel בפעם הראשונה שהמערכת קוראת לשיטה onCreate() של אובייקט פעילות. יכול להיות שהמערכת תקרא ל-onCreate() כמה פעמים במהלך הפעילות, למשל כשמסובבים את המסך של המכשיר. ה-ViewModel קיים מהרגע שבו מבקשים ViewModel ועד שהפעילות מסתיימת ומושמדת.

ניקוי התלויות של ViewModel

ה-ViewModel קורא לשיטה onCleared כשהמערכת ViewModelStoreOwner משמידה אותו במהלך מחזור החיים שלו. כך תוכלו לנקות את כל העבודות או התלות שמתבצעות במהלך מחזור החיים של ViewModel.

בדוגמה הבאה מוצגת חלופה ל-viewModelScope. ‫viewModelScope הוא CoroutineScope מובנה שעוקב אוטומטית אחרי מחזור החיים של ViewModel. ה-ViewModel משתמש בו כדי להפעיל פעולות שקשורות לעסק. אם רוצים להשתמש בהיקף מותאם אישית במקום ב-viewModelScope כדי לבדוק בקלות רבה יותר, אפשר להעביר את CoroutineScope ל-ViewModel כתלות בקונסטרוקטור שלו. כש-ViewModelStoreOwner מנקה את ViewModel בסוף מחזור החיים שלו, ViewModel מבטל גם את CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

מגרסה 2.5 של מחזור החיים ואילך, אפשר להעביר אובייקט אחד או יותר Closeable לבנאי של ViewModel שנסגר אוטומטית כשמוסר המופע של ViewModel.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

שיטות מומלצות

ריכזנו כאן כמה שיטות מומלצות שכדאי לפעול לפיהן כשמטמיעים ViewModel:

  • בגלל ההיקף שלהם, כדאי להשתמש ב-ViewModels כפרטי הטמעה של מאחסן מצב ברמת המסך. אל תשתמשו בהם כמחזיקי מצב של רכיבי ממשק משתמש לשימוש חוזר, כמו קבוצות צ'יפים או טפסים. אחרת, תקבלו את אותו מופע ViewModel בשימושים שונים של אותו רכיב ממשק משתמש באותו ViewModelStoreOwner, אלא אם תשתמשו במפתח ViewModel מפורש לכל צ'יפ.
  • מודלים של תצוגה לא אמורים לדעת פרטים על הטמעת ממשק המשתמש. חשוב לשמור על שמות השיטות שה-ViewModel API חושף ועל שמות השדות של מצב ממשק המשתמש גנריים ככל האפשר. כך, ה-ViewModel יכול להתאים לכל סוג של ממשק משתמש: טלפון נייד, מכשיר מתקפל, טאבלט ואפילו Chromebook!
  • יכול להיות שהם יפעלו יותר זמן מ-ViewModelStoreOwner, ולכן לא כדאי ש-ViewModels יכילו הפניות ל-API שקשורות למחזור החיים, כמו Context או Resources, כדי למנוע דליפות זיכרון.
  • אל תעבירו ViewModels למחלקות אחרות, לפונקציות או לרכיבים אחרים של ממשק המשתמש. מכיוון שהפלטפורמה מנהלת אותם, כדאי להשתמש בהם כמה שיותר קרוב לפעילות, לפונקציה הניתנת להרכבה ברמת המסך או ליעד הניווט. כך רכיבים ברמה נמוכה יותר לא יכולים לגשת ליותר נתונים ולוגיקה ממה שהם צריכים.

מידע נוסף

ככל שהנתונים נעשים מורכבים יותר, יכול להיות שתבחרו להשתמש במחלקה נפרדת רק כדי לטעון את הנתונים. המטרה של ViewModel היא להצפין את הנתונים עבור בקר ממשק משתמש, כדי שהנתונים לא יימחקו כשמבצעים שינויים בהגדרות. במאמר שמירת מצבי ממשק המשתמש מוסבר איך לטעון, לשמור ולנהל נתונים כשמשנים את ההגדרות.

במדריך לארכיטקטורה של אפליקציות Android מוצע ליצור מחלקת מאגר כדי לטפל בפונקציות האלה.

מקורות מידע נוספים

למידע נוסף על מחלקת ViewModel, אפשר לעיין במקורות המידע הבאים.

תיעוד

צפייה בתוכן

דוגמאות