סקירה כללית של ViewModel חלק מ-Android Jetpack.
המחלקות 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
בפעם הראשונה שהמערכת קוראת לשיטה 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
, אפשר לעיין במקורות המידע הבאים.
מסמכים
טעימות
מומלץ
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- שימוש ב-Kotlin coroutines עם רכיבים שמודעים למחזור החיים
- שמירת מצבי ממשק המשתמש
- טעינה והצגה של נתונים עם חלוקה לדפים