שמירת מצבים בממשק המשתמש

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ViewModel מצב שמור אחסון קבוע
מיקום אחסון בזיכרון בזיכרון בדיסק או ברשת
השינוי נשמר גם אחרי שינוי בהגדרה כן כן כן
התהליך ממשיך לפעול גם אם המערכת משביתה אותו לא כן כן
ההודעה ממשיכה להופיע גם אחרי שהמשתמש סוגר את המסך/finish() לא לא כן
מגבלות על הנתונים אפשר להשתמש באובייקטים מורכבים, אבל המקום מוגבל בגלל הזיכרון הזמין רק לסוגים פרימיטיביים ולאובייקטים פשוטים וקטנים כמו String מוגבל רק על ידי נפח האחסון או העלות / הזמן של השליפה ממקור הרשת
זמן קריאה/כתיבה מהיר (גישה לזיכרון בלבד) איטי (נדרשת סריאליזציה/דה-סריאליזציה) איטי (נדרשת גישה לדיסק או טרנזקציה ברשת)

שימוש ב-ViewModel כדי לטפל בשינויים בהגדרות

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

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

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

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

שימוש במצב שמור כגיבוי לטיפול בהשבתת תהליך שהמערכת יזמה

ממשקי API כמו rememberSerializable ו-rememberSaveable ב-Compose ו-SavedStateHandle ב-ViewModels שומרים את הנתונים שנדרשים לטעינה מחדש של מצב ממשק המשתמש אם המערכת משמידה רכיב ואחר כך יוצרת אותו מחדש. כדי לטפל במבני נתונים מורכבים בצורה יעילה יותר, SavedStateHandle תומך ב-Kotlinx Serialization באמצעות התוסף saved {}. כך אפשר לשמור ולשחזר בצורה חלקה אובייקטים בטוחים לסוגים לצד סוגים פרימיטיביים רגילים. במאמר מצב ו-Jetpack פיתוח נייטיב מוסבר איך להטמיע מצב שמור באמצעות rememberSaveable.

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

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

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

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

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

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

שימוש ב-SavedStateRegistry כדי להתחבר למצב השמור

החל מ-Fragment 1.1.0 או מהתלות הטרנזיטיבית שלו Activity 1.0.0, רכיבי ממשק משתמש, כמו ComponentActivity, מטמיעים את SavedStateRegistryOwner ומספקים SavedStateRegistry שקשור לרכיב הזה. ‫SavedStateRegistry מאפשר לרכיבים להתחבר למצב השמור כדי להשתמש בו או לתרום לו. לדוגמה, המודול Saved State for ViewModel משתמש ב-SavedStateRegistry כדי ליצור SavedStateHandle ולספק אותו לאובייקטים ViewModel. אפשר לאחזר את SavedStateRegistry מתוך הבעלים של מחזור החיים על ידי קריאה ל-savedStateRegistry.

רכיבים שתורמים למצב השמור חייבים להטמיע את SavedStateRegistry.SavedStateProvider, שמגדיר שיטה אחת בשם saveState(). השיטה saveState() מאפשרת לרכיב להחזיר Bundle שמכיל את כל המצב שצריך לשמור מהרכיב הזה. ‫SavedStateRegistry קוראת לשיטה הזו במהלך שלב שמירת המצב במחזור החיים של הבעלים של מחזור החיים.

  class SearchManager : SavedStateRegistry.SavedStateProvider {
      companion object {
          private const val QUERY = "query"
      }

      private val query: String? = null

      ...

      override fun saveState(): Bundle {
          return bundleOf(QUERY to query)
      }
  }

כדי לרשום SavedStateProvider, מתקשרים אל registerSavedStateProvider() ב-SavedStateRegistry, ומעבירים מפתח לשיוך לנתונים של הספק וגם את הספק. אפשר לאחזר את הנתונים שנשמרו קודם של הספק מהמצב השמור על ידי קריאה ל-consumeRestoredStateForKey() ב-SavedStateRegistry, והעברת המפתח שמשויך לנתונים של הספק.

במהלך ComponentActivity, אפשר לרשום SavedStateProvider בonCreate() אחרי שמתקשרים אל super.onCreate(). אפשר גם להגדיר LifecycleObserver ב-SavedStateRegistryOwner, שמטמיע את LifecycleOwner, ולרשום את SavedStateProvider ברגע שמתרחש האירוע ON_CREATE. באמצעות LifecycleObserver, אפשר להפריד בין הרישום והאחזור של המצב שנשמר קודם לבין SavedStateRegistryOwner עצמו.

  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 SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

שימוש בנתונים מקומיים כדי לטפל בהשבתת תהליך במקרה של נתונים מורכבים או גדולים

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

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

ניהול מצב ממשק המשתמש: חלוקה ופתרון

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

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

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

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

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

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

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

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

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

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

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

Codelabs

צפייה בתוכן