מושגים ויישום ב-Jetpack פיתוח נייטיב
במדריך הזה נסביר מהן הציפיות של המשתמשים לגבי מצב ממשק המשתמש, ומהן האפשרויות לשמירת המצב.
שמירה ושחזור של מצב ממשק המשתמש של פעילות במהירות אחרי שהמערכת משמידה פעילויות או אפליקציות חיוניים לחוויית משתמש טובה. המשתמשים מצפים שמצב ממשק המשתמש יישאר זהה, אבל המערכת עשויה להרוס את הפעילות ואת המצב המאוחסן שלה.
כדי לגשר על הפער בין ציפיות המשתמשים לבין התנהגות המערכת, אפשר להשתמש בשילוב של השיטות הבאות:
- אובייקטים של
ViewModel. - מצבי מופע שנשמרו בהקשרים הבאים:
- צפיות:
onSaveInstanceState()API. - ViewModels:
SavedStateHandle.
- צפיות:
- אחסון מקומי כדי לשמור את מצב ממשק המשתמש במהלך מעברים בין אפליקציות ופעילויות.
הפתרון האופטימלי תלוי במורכבות של נתוני ממשק המשתמש, בתרחישי השימוש באפליקציה ובאיזון בין מהירות הגישה לנתונים לבין השימוש בזיכרון.
חשוב לוודא שהאפליקציה עומדת בציפיות של המשתמשים ומציעה ממשק מהיר ורספונסיבי. כדי למנוע עיכובים בטעינת הנתונים בממשק המשתמש, במיוחד אחרי שינויים נפוצים בהגדרות כמו רוטציה.
ציפיות המשתמשים והתנהגות המערכת
בהתאם לפעולה שהמשתמש מבצע, הוא מצפה שמצב הפעילות יימחק או יישמר. במקרים מסוימים, המערכת מבצעת באופן אוטומטי את מה שהמשתמש מצפה. במקרים אחרים, המערכת עושה את הפעולה ההפוכה.
סגירת מצב ממשק המשתמש ביוזמת המשתמש
המשתמש מצפה שכשהוא מתחיל פעילות, מצב ממשק המשתמש הזמני של הפעילות הזו יישאר זהה עד שהמשתמש יסגור את הפעילות לגמרי. המשתמש יכול לסגור לגמרי את הפעילות באופן הבא:
- החלקה של הפעילות מחוץ למסך הסקירה הכללית (הפריטים האחרונים).
- סגירה או יציאה ידנית של האפליקציה ממסך ההגדרות.
- הפעלת המכשיר מחדש.
- השלמת פעולה מסוימת של 'סיום' (שמגובה על ידי
Activity.finish()).
ההנחה של המשתמשים במקרים של סגירה מלאה היא שהם עברו באופן קבוע מהפעילות, ואם הם יפתחו מחדש את הפעילות הם מצפים שהיא תתחיל ממצב נקי. ההתנהגות הבסיסית של המערכת בתרחישי סגירה כאלה תואמת לציפיות המשתמשים – מופע הפעילות ייהרס ויוסר מהזיכרון, יחד עם כל מצב שמאוחסן בו וכל רשומה של מצב מופע שנשמר שמשויכת לפעילות.
יש כמה חריגים לכלל הזה לגבי סגירה מלאה – לדוגמה, יכול להיות שמשתמש יצפה שהדפדפן יעביר אותו לדף האינטרנט המדויק שהוא צפה בו לפני שהוא יצא מהדפדפן באמצעות הכפתור "הקודם".
סגירה של מצב ממשק המשתמש שהופעל על ידי המערכת
משתמש מצפה שמצב ממשק המשתמש של פעילות מסוימת יישאר זהה לאורך שינוי בהגדרות, כמו סיבוב או מעבר למצב מרובה חלונות. עם זאת, כברירת מחדל המערכת משמידה את הפעילות כשמתרחש שינוי כזה בהגדרות, ומבטלת כל מצב של ממשק משתמש שמאוחסן במופע הפעילות. מידע נוסף על הגדרות המכשיר זמין בדף חומר העזר בנושא הגדרות.
הערה: אפשר (אבל לא מומלץ) לבטל את התנהגות ברירת המחדל לגבי שינויים בהגדרות. פרטים נוספים זמינים במאמר בנושא טיפול בשינוי ההגדרה.
משתמשים מצפים גם שמצב ממשק המשתמש של הפעילות יישאר זהה אם הם עוברים באופן זמני לאפליקציה אחרת ואז חוזרים לאפליקציה שלכם מאוחר יותר. לדוגמה, המשתמש מבצע חיפוש בפעילות החיפוש ואז לוחץ על הכפתור הראשי או עונה לשיחה – כשהוא חוזר לפעילות החיפוש הוא מצפה למצוא שם את מילת המפתח לרשת החיפוש והתוצאות, בדיוק כמו קודם.
בתרחיש הזה, האפליקציה שלכם מוצבת ברקע, והמערכת עושה כמיטב יכולתה כדי לשמור את תהליך האפליקציה בזיכרון. עם זאת, יכול להיות שהמערכת תסיים את תהליך האפליקציה בזמן שהמשתמש לא נמצא בה ומשתמש באפליקציות אחרות. במקרה כזה, מופסקת הפעילות של המופע, וכל הנתונים שנשמרו בו נמחקים. כשהמשתמש מפעיל מחדש את האפליקציה, הפעילות נמצאת במצב נקי באופן לא צפוי. מידע נוסף על השבתת תהליך זמין במאמר תהליכים ומחזור החיים של אפליקציות.
אפשרויות לשמירת מצב ממשק המשתמש
אם הציפיות של המשתמש לגבי מצב ממשק המשתמש לא תואמות להתנהגות ברירת המחדל של המערכת, צריך לשמור ולשחזר את מצב ממשק המשתמש של המשתמש כדי לוודא שההרס שיזמה המערכת יהיה שקוף למשתמש.
כל אחת מהאפשרויות לשמירת מצב ממשק המשתמש שונה מהאחרות לפי המאפיינים הבאים, שמשפיעים על חוויית המשתמש:
ViewModel |
מצב המופע שנשמר |
אחסון קבוע |
|
מיקום אחסון |
בזיכרון |
בזיכרון |
בדיסק או ברשת |
השינוי נשמר גם אחרי שינוי בהגדרה |
כן |
כן |
כן |
התהליך ממשיך לפעול גם אם המערכת משביתה אותו |
לא |
כן |
כן |
הפעילות נמשכת גם אחרי שהמשתמש סוגר את הפעילות או מסיים אותה |
לא |
לא |
כן |
מגבלות על הנתונים |
אפשר להשתמש באובייקטים מורכבים, אבל המקום מוגבל בגלל הזיכרון הזמין |
רק לסוגים פרימיטיביים ולאובייקטים פשוטים וקטנים כמו |
מוגבל רק על ידי נפח האחסון או העלות / הזמן של השליפה ממקור הרשת |
זמן קריאה/כתיבה |
מהיר (גישה לזיכרון בלבד) |
איטי (נדרשת סריאליזציה/דה-סריאליזציה) |
איטי (נדרשת גישה לדיסק או טרנזקציה ברשת) |
שימוש ב-ViewModel כדי לטפל בשינויים בהגדרות
ViewModel הוא פתרון אידיאלי לאחסון ולניהול של נתונים שקשורים לממשק המשתמש בזמן שהמשתמש משתמש באפליקציה באופן פעיל. הוא מאפשר גישה מהירה לנתוני ממשק המשתמש ועוזר לכם להימנע מאחזור מחדש של נתונים מהרשת או מהדיסק במהלך סיבוב, שינוי גודל החלון ושינויים נפוצים אחרים בהגדרות. במדריך בנושא ViewModel מוסבר איך להטמיע ViewModel.
ה-ViewModel שומר את הנתונים בזיכרון, ולכן עלות השליפה שלו נמוכה יותר מעלות השליפה של נתונים מהדיסק או מהרשת. ViewModel משויך לפעילות (או לבעלים אחרים של מחזור החיים) – הוא נשאר בזיכרון במהלך שינוי בהגדרות, והמערכת משייכת אוטומטית את ה-ViewModel למופע הפעילות החדש שנוצר כתוצאה מהשינוי בהגדרות.
המערכת משמידה את ה-ViewModels באופן אוטומטי כשהמשתמש יוצא מהפעילות או מהקטע, או אם קוראים ל-finish(), כלומר המצב מתנקה כמו שהמשתמש מצפה בתרחישים האלה.
בניגוד למצב שמור של מופע, אובייקטים מסוג ViewModel מושמדים במהלך השבתת תהליך שהמערכת יזמה. כדי לטעון מחדש נתונים אחרי השבתת תהליך שהמערכת יזמה ב-ViewModel, משתמשים ב-SavedStateHandle API. לחלופין, אם הנתונים קשורים לממשק המשתמש ואין צורך לשמור אותם ב-ViewModel, אפשר להשתמש ב-onSaveInstanceState(). אם הנתונים הם נתוני אפליקציה, כדאי לשמור אותם בדיסק.
אם כבר יש לכם פתרון בזיכרון לאחסון מצב ממשק המשתמש שלכם במהלך שינויים בהגדרות, יכול להיות שלא תצטרכו להשתמש ב-ViewModel.
שימוש במצב שמור של מופע כגיבוי לטיפול בהשבתת תהליך שהמערכת יזמה
הקריאה החוזרת (callback) onSaveInstanceState() במערכת View ו-SavedStateHandle ב-ViewModels מאחסנת נתונים שנדרשים לטעינה מחדש של מצב בקר ממשק המשתמש, כמו פעילות או קטע, אם המערכת משמידה את הבקר הזה ויוצרת אותו מחדש מאוחר יותר. במאמר שמירה ושחזור של מצב פעילות במדריך למחזור החיים של פעילות מוסבר איך להטמיע מצב שמור של מופע באמצעות onSaveInstanceState.
חבילות של מצב מופע שנשמרו נשמרות גם אחרי שינויים בהגדרות וגם אחרי השבתת תהליך, אבל הן מוגבלות על ידי האחסון והמהירות, כי ממשקי ה-API השונים מבצעים סריאליזציה של הנתונים. אם האובייקטים שעוברים סריאליזציה הם מורכבים, התהליך הזה יכול לצרוך הרבה זיכרון. התהליך הזה מתרחש ב-Thread הראשי במהלך שינוי בהגדרות, ולכן סריאליזציה ארוכת טווח יכולה לגרום להשמטת פריימים ולגמגום חזותי.
אל תשתמשו במצב שמור של מופע כדי לאחסן כמויות גדולות של נתונים, כמו מפות סיביות, או מבני נתונים מורכבים שדורשים סריאליזציה או ביטול סריאליזציה ארוכים. במקום זאת, כדאי לאחסן רק סוגים פרימיטיביים ואובייקטים פשוטים וקטנים, כמו String. לכן, כדאי להשתמש במצב שמור של מופע כדי לאחסן כמות מינימלית של נתונים נחוצים, כמו מזהה, כדי ליצור מחדש את הנתונים שנדרשים לשחזור ממשק המשתמש למצב הקודם שלו אם מנגנוני ההתמדה האחרים ייכשלו. רוב האפליקציות צריכות ליישם את הפתרון הזה כדי לטפל בהשבתת תהליך שהמערכת יזמה.
יכול להיות שלא תצטרכו להשתמש במצב מופע שמור בכלל, בהתאם לתרחישי השימוש באפליקציה. לדוגמה, דפדפן עשוי להחזיר את המשתמש לדף האינטרנט המדויק שבו הוא צפה לפני שיצא מהדפדפן. אם הפעילות מתנהגת בצורה הזו, אפשר לוותר על השימוש במצב מופע שמור ולשמור את כל הנתונים באופן מקומי.
בנוסף, כשפותחים פעילות מ-Intent, חבילת התוספים מועברת לפעילות גם כשמשנים את ההגדרה וגם כשהמערכת משחזרת את הפעילות.
בכל אחד מהתרחישים האלה, עדיין כדאי להשתמש ב-ViewModel כדי למנוע בזבוז של מחזורים בטעינה מחדש של נתונים מהמסד במהלך שינוי בהגדרות.
במקרים שבהם נתוני ממשק המשתמש שרוצים לשמור הם פשוטים וקלים, אפשר להשתמש רק בממשקי API של מצב מופע שנשמר כדי לשמור את נתוני המצב.
שימוש ב-SavedStateRegistry כדי להתחבר למצב השמור
החל מ-Fragment 1.1.0 או מהתלות הטרנזיטיבית שלו Activity
1.0.0, בקרי ממשק משתמש, כמו Activity או Fragment, מטמיעים את SavedStateRegistryOwner ומספקים SavedStateRegistry שקשור לבקר הזה. SavedStateRegistry מאפשר לרכיבים להתחבר למצב השמור של בקר ממשק המשתמש כדי להשתמש בו או לתרום לו. לדוגמה, המודול Saved State for ViewModel משתמש ב-SavedStateRegistry כדי ליצור SavedStateHandle ולספק אותו לאובייקטים ViewModel. אפשר לאחזר את SavedStateRegistry מתוך בקר ממשק המשתמש על ידי קריאה ל-getSavedStateRegistry.
רכיבים שתורמים למצב השמור חייבים להטמיע את SavedStateRegistry.SavedStateProvider, שמגדיר שיטה אחת בשם saveState. השיטה saveState() מאפשרת לרכיב להחזיר Bundle שמכיל את כל המצב שצריך לשמור מהרכיב הזה.
SavedStateRegistry קורא ל-method הזו במהלך שלב שמירת המצב במחזור החיים של בקר ממשק המשתמש.
class SearchManager implements SavedStateRegistry.SavedStateProvider {
private static String QUERY = "query";
private String query = null;
...
@NonNull
@Override
public Bundle saveState() {
Bundle bundle = new Bundle();
bundle.putString(QUERY, query);
return bundle;
}
}
כדי לרשום SavedStateProvider, מתקשרים אל registerSavedStateProvider() ב-SavedStateRegistry, ומעבירים מפתח לשיוך לנתונים של הספק וגם את הספק. אפשר לאחזר את הנתונים שנשמרו קודם של הספק מהמצב השמור על ידי קריאה ל-consumeRestoredStateForKey() ב-SavedStateRegistry, והעברת המפתח שמשויך לנתונים של הספק.
בתוך Activity או Fragment, אפשר לרשום SavedStateProvider בonCreate() אחרי שמתקשרים אל super.onCreate(). אפשר גם להגדיר LifecycleObserver ב-SavedStateRegistryOwner, שמטמיע את LifecycleOwner, ולרשום את SavedStateProvider ברגע שמתרחש האירוע ON_CREATE. באמצעות LifecycleObserver, אפשר להפריד בין הרישום והאחזור של המצב שנשמר קודם לבין SavedStateRegistryOwner עצמו.
Kotlin
class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
companion object {
private const val PROVIDER = "search_manager"
private const val QUERY = "query"
}
private val query: String? = null
init {
// Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
// Register this object for future calls to saveState()
registry.registerSavedStateProvider(PROVIDER, this)
// Get the previously saved state and restore it
val state = registry.consumeRestoredStateForKey(PROVIDER)
// Apply the previously saved state
query = state?.getString(QUERY)
}
}
}
override fun saveState(): Bundle {
return bundleOf(QUERY to query)
}
...
}
class SearchFragment : Fragment() {
private var searchManager = SearchManager(this)
...
}
Java
class SearchManager implements SavedStateRegistry.SavedStateProvider {
private static String PROVIDER = "search_manager";
private static String QUERY = "query";
private String query = null;
public SearchManager(SavedStateRegistryOwner registryOwner) {
registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
if (event == Lifecycle.Event.ON_CREATE) {
SavedStateRegistry registry = registryOwner.getSavedStateRegistry();
// Register this object for future calls to saveState()
registry.registerSavedStateProvider(PROVIDER, this);
// Get the previously saved state and restore it
Bundle state = registry.consumeRestoredStateForKey(PROVIDER);
// Apply the previously saved state
if (state != null) {
query = state.getString(QUERY);
}
}
});
}
@NonNull
@Override
public Bundle saveState() {
Bundle bundle = new Bundle();
bundle.putString(QUERY, query);
return bundle;
}
...
}
class SearchFragment extends Fragment {
private SearchManager searchManager = new SearchManager(this);
...
}
שימוש בנתונים מקומיים כדי לטפל בהשבתת תהליך במקרה של נתונים מורכבים או גדולים
אחסון מקומי קבוע, כמו מסד נתונים או העדפות משותפות, יישמר כל עוד האפליקציה מותקנת במכשיר של המשתמש (אלא אם המשתמש מוחק את הנתונים של האפליקציה). אחסון מקומי כזה שורד פעילות שמערכת יוזמת והשבתת תהליך של האפליקציה, אבל יכול להיות יקר לאחזר אותו כי צריך לקרוא אותו מהאחסון המקומי לזיכרון. לרוב, האחסון המקומי הקבוע הזה כבר מהווה חלק מארכיטקטורת האפליקציה, כדי לאחסן את כל הנתונים שלא רוצים לאבד אם פותחים וסוגרים את הפעילות.
ViewModel או מצב מופע שמור הם לא פתרונות אחסון לטווח ארוך, ולכן הם לא מחליפים אחסון מקומי, כמו מסד נתונים. במקום זאת, צריך להשתמש במנגנונים האלה רק לאחסון זמני של מצב ממשק המשתמש, ולהשתמש באחסון קבוע לנתוני האפליקציה. במדריך לארכיטקטורת אפליקציות מוסבר איך להשתמש באחסון מקומי כדי לשמור את נתוני מודל האפליקציה לטווח ארוך (למשל, אחרי הפעלה מחדש של המכשיר).
ניהול מצב ממשק המשתמש: חלוקה ופתרון
כדי לשמור ולשחזר את מצב ממשק המשתמש בצורה יעילה, אפשר לחלק את העבודה בין סוגים שונים של מנגנוני שמירה. ברוב המקרים, כל אחד מהמנגנונים האלה צריך לאחסן סוג אחר של נתונים שמשמשים בפעילות, בהתאם לשיקולים של מורכבות הנתונים, מהירות הגישה ומשך החיים:
- שמירה מקומית: מאחסנת את כל נתוני האפליקציה שלא רוצים לאבד אם פותחים וסוגרים את הפעילות.
- דוגמה: אוסף של אובייקטים של שירים, שיכול לכלול קובצי אודיו ומטא-נתונים.
-
ViewModel: מאחסן בזיכרון את כל הנתונים שנדרשים להצגת ממשק המשתמש המשויך, מצב ממשק המשתמש של המסך.- דוגמה: אובייקטים של שירים מהחיפוש האחרון ושאילתת החיפוש האחרונה.
- מצב שמור של מופע: מאחסן כמות קטנה של נתונים שנדרשים לטעינה מחדש של מצב ממשק המשתמש אם המערכת מפסיקה ואז יוצרת מחדש את ממשק המשתמש. במקום לאחסן כאן אובייקטים מורכבים, כדאי לשמור את האובייקטים המורכבים באחסון מקומי ולאחסן מזהה ייחודי לאובייקטים האלה בממשקי ה-API של מצב המופע השמור.
- דוגמה: אחסון של שאילתת החיפוש האחרונה.
לדוגמה, פעילות שמאפשרת לכם לחפש בספריית השירים שלכם. כך צריך לטפל באירועים שונים:
כשהמשתמש מוסיף שיר, ViewModel מעביר מיד את האחריות לאחסון הנתונים האלה באופן מקומי. אם רוצים שהשיר החדש שנוסף יוצג בממשק המשתמש, צריך לעדכן את הנתונים באובייקט ViewModel כך שישקפו את הוספת השיר. חשוב לזכור לבצע את כל ההוספות למסד הנתונים מחוץ לשרשור הראשי.
כשמשתמש מחפש שיר, כל נתוני השיר המורכבים שנטענים ממסד הנתונים צריכים להישמר באופן מיידי באובייקט ViewModel כחלק ממצב ממשק המשתמש של המסך.
כשהפעילות עוברת לרקע והמערכת קוראת לממשקי ה-API של מצב המופע השמור, שאילתת החיפוש צריכה להישמר במצב המופע השמור, למקרה שהתהליך ייווצר מחדש. המידע הזה נחוץ כדי לטעון נתוני אפליקציה שנשמרו, ולכן צריך לאחסן את שאילתת החיפוש ב-ViewModel SavedStateHandle. זה כל המידע שצריך כדי לטעון את הנתונים ולהחזיר את ממשק המשתמש למצב הנוכחי שלו.
שחזור מצבים מורכבים: הרכבת החלקים
כשמגיע הזמן שהמשתמש יחזור לפעילות, יש שני תרחישים אפשריים ליצירה מחדש של הפעילות:
- הפעילות נוצרת מחדש אחרי שהמערכת הפסיקה אותה. המערכת שומרת את השאילתה בחבילת מצב מופע שמור, וממשק המשתמש צריך להעביר את השאילתה אל
ViewModelאם לא נעשה שימוש ב-SavedStateHandle. ה-ViewModelרואה שאין תוצאות חיפוש שמורות במטמון, ומטיל על רכיב אחר את האחריות לטעינת תוצאות החיפוש באמצעות שאילתת החיפוש שצוינה. - הפעילות נוצרת מחדש אחרי שינוי בהגדרות. מכיוון שמופע
ViewModelלא נהרס, ל-ViewModelיש את כל המידע ששמור במטמון בזיכרון, והוא לא צריך לשלוח שוב שאילתה למסד הנתונים.
מקורות מידע נוספים
מידע נוסף על שמירת מצבי ממשק המשתמש זמין במקורות המידע הבאים.
בלוגים
- ViewModels: דוגמה פשוטה
- ViewModels: Persistence,
onSaveInstanceState, Restoring UI State and Loaders