סקירה כללית של ViewModel   חלק מ-Android Jetpack.

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

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

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

היתרונות של ViewModel

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

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

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

התמדה

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

היקף

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

טווח של מחלקות הן מחלקות משנה ישירות או עקיפות של הממשק ViewModelStoreOwner. מחלקות המשנה הישירות הן ComponentActivity,‏ Fragment ו-NavBackStackEntry. רשימה מלאה של מחלקות משנה עקיפות זמינה בחומר העזר בנושא ViewModelStoreOwner.

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

מידע נוסף זמין בקטע מחזור החיים של ViewModel שבהמשך.

SavedStateHandle

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

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

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

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

Jetpack פיתוח נייטיב

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

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

כדי ליהנות מהיתרונות של ViewModel ב-Compose, צריך לארח כל מסך ב-Fragment או ב-Activity, או להשתמש ב-Compose Navigation ולהשתמש ב-ViewModels בפונקציות שאפשר להוסיף להן קומפוזיציה כמה שיותר קרוב ליעד הניווט. הסיבה לכך היא שאפשר להגדיר את ה-ViewModel ליעדי ניווט, לתרשימי ניווט, לפעילויות ול-Fragments.

מידע נוסף זמין במדריך בנושא העברת מצב ב-Jetpack Compose.

הטמעה של ViewModel

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

Kotlin

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,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

אחרי זה אפשר לגשת ל-ViewModel מפעילות באופן הבא:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack פיתוח נייטיב

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
}

שימוש ב-coroutines עם ViewModel

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

מידע נוסף זמין במאמר שימוש ב-Kotlin coroutines עם רכיבי Android Architecture.

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

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

  • במקרה של פעילות, כשהיא מסתיימת.
  • במקרה של קטע, כשהוא מנותק.
  • במקרה של רשומה של ניווט, כשהיא מוסרת ממחסנית החזרה.

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

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

איור שמציג את מחזור החיים של 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 בשימושים שונים של אותו רכיב UI באותו ViewModelStoreOwner, אלא אם תשתמשו במפתח ViewModel מפורש לכל צ'יפ.
  • מודלים של תצוגה לא אמורים לדעת פרטים על הטמעת ממשק המשתמש. חשוב לשמור על שמות גנריים ככל האפשר של ה-methods שה-ViewModel API חושף ושל שדות מצב ממשק המשתמש. כך, ה-ViewModel יכול להתאים לכל סוג של ממשק משתמש: טלפון נייד, מכשיר מתקפל, טאבלט או אפילו Chromebook.
  • יכול להיות שהם יפעלו יותר זמן מ-ViewModelStoreOwner, ולכן ב-ViewModels לא צריכות להיות הפניות ל-API שקשורות למחזור החיים, כמו Context או Resources, כדי למנוע דליפות זיכרון.
  • אל תעבירו ViewModels למחלקות אחרות, לפונקציות או לרכיבים אחרים של ממשק המשתמש. הפלטפורמה מנהלת את המשאבים האלה, ולכן כדאי לשמור אותם קרוב ככל האפשר לפלטפורמה. קרוב לפונקציה הניתנת להגדרה ברמת הפעילות, הרכיב או המסך. כך רכיבים ברמה נמוכה יותר לא יכולים לגשת ליותר נתונים ולוגיקה ממה שהם צריכים.

מידע נוסף

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

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

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

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

מסמכים

טעימות