שמירת מצבי ממשק משתמש (תצוגות)

מושגים ויישום ב-Jetpack Compose

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

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

כדי לגשר על הפער בין ציפיות המשתמשים לבין התנהגות המערכת, אפשר להשתמש בשילוב של השיטות הבאות:

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

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

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

ציפיות המשתמשים והתנהגות המערכת

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

סגירת מצב ממשק המשתמש ביוזמת המשתמש

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

  • החלקה של הפעילות מחוץ למסך הסקירה הכללית (האחרונים).
  • סגירה או סגירה בכוח של האפליקציה ממסך ההגדרות.
  • הפעלת מחדש של המכשיר.
  • השלמת פעולת 'סיום' כלשהי (שמגובה על ידי Activity.finish()).

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

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

סגירה של מצב ממשק המשתמש שהופעל על ידי המערכת

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

הערה: אפשר (אבל לא מומלץ) לבטל את התנהגות ברירת המחדל לגבי שינויים בהגדרות. פרטים נוספים זמינים במאמר בנושא טיפול בשינוי ההגדרה.

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

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

אפשרויות לשמירת מצב ממשק המשתמש

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

כל אחת מהאפשרויות לשמירת מצב ממשק המשתמש משתנה בהתאם למאפיינים הבאים שמשפיעים על חוויית המשתמש:

ViewModel

מצב המכונה השמורה

אחסון קבוע

מיקום אחסון

בזיכרון

בזיכרון

בדיסק או ברשת

הנתונים נשמרים גם אחרי שינוי בהגדרות

כן

כן

כן

התהליך ממשיך לפעול גם אם המערכת משביתה אותו

לא

כן

כן

הפעילות נמשכת גם אחרי שהמשתמש סוגר את האפליקציה או מסיים את הפעילות

לא

לא

כן

מגבלות נתונים

אפשר להשתמש באובייקטים מורכבים, אבל המקום מוגבל בזיכרון הזמין

רק לסוגים פרימיטיביים ולאובייקטים פשוטים וקטנים כמו String

מוגבל רק על ידי נפח האחסון או העלות / הזמן של השליפה ממקור הרשת

זמן קריאה/כתיבה

מהיר (גישה לזיכרון בלבד)

איטי (נדרשת סריאליזציה/דה-סריאליזציה)

איטי (נדרשת גישה לדיסק או טרנזקציה ברשת)

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

Activity
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, ולכן צריך לאחסן את שאילתת החיפוש ב-ViewModel SavedStateHandle. זה כל המידע שצריך כדי לטעון את הנתונים ולהחזיר את ממשק המשתמש למצב הנוכחי שלו.

שחזור מצבים מורכבים: הרכבת החלקים

כשהמשתמש צריך לחזור לפעילות, יש שני תרחישים אפשריים ליצירה מחדש של הפעילות:

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

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

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

בלוגים

Codelabs