סקירה כללית של 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 ישירות לרכיב שאפשר להרכיב באמצעות rememberViewModelStoreOwner API. ה-ViewModel שלכם מוגדר בהמשך למחזור החיים של ViewModelStoreOwner. הוא נשאר בזיכרון עד שViewModelStoreOwner נעלם באופן סופי (למשל, כשהבעלים של הרכיב שניתן להרכבה יוצא מההרכבה).

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

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

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

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 שאליו הוא משויך נעלם. מצב כזה יכול לקרות בהקשרים הבאים:

  • במקרה של פעילות, כשהיא מסתיימת.
  • במקרה של רשומה ב-Navigation, כשהיא מוסרת מ-back stack.
  • במקרה של רכיב קומפוזבילי, כשהוא יוצא מהקומפוזיציה. אפשר להשתמש ב-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 יכללו הפניות ל-APIs שקשורים למחזור החיים, כמו Context או Resources, כדי למנוע דליפות זיכרון.
  • אל תעבירו ViewModels למחלקות אחרות, לפונקציות או לרכיבים אחרים של ממשק המשתמש. מכיוון שהפלטפורמה מנהלת אותם, כדאי למקם אותם כמה שיותר קרוב לפלטפורמה – קרוב ל-Activity, לפונקציה קומפוזבילית ברמת המסך או ליעד ה-Navigation. כך נמנעת גישה של רכיבים ברמה נמוכה יותר ליותר נתונים ולוגיקה ממה שהם צריכים.

מידע נוסף

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

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

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

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

מאמרי עזרה

צפיות בתוכן

דוגמאות