שמירת המצב של ממשק המשתמש ב'כתיבה'

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

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

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

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

הלוגיקה של ממשק המשתמש

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

בקטע הקוד הבא, rememberSaveable משמש לאחסון של מצב רכיב בוליאני יחיד בממשק המשתמש:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

איור 1. בועת ההודעה בצ'אט מתרחבת ומתכווצת כשמקישים עליה.

showDetails הוא משתנה בוליאני שמאחסן את המצב של בועת הצ'אט – מכווצת או מורחבת.

rememberSaveable שומרת את המצב של רכיב ממשק המשתמש ב-Bundle באמצעות מנגנון שמירת מצב המופע.

הוא יכול לאחסן סוגים פרימיטיביים בחבילה באופן אוטומטי. אם המצב שלכם מוחזק בסוג שהוא לא פרימיטיבי, כמו מחלקת נתונים, אתם יכולים להשתמש במנגנוני אחסון שונים, כמו שימוש בהערה Parcelize, שימוש בממשקי API של Compose כמו listSaver ו-mapSaver, או הטמעה של מחלקת שמירה מותאמת אישית שמרחיבה את מחלקת זמן הריצה של Compose‏ Saver. מידע נוסף על השיטות האלה זמין במאמר דרכים לאחסון מצב.

בקטע הקוד הבא, ה-API של rememberLazyListState Compose מאחסן את LazyListState, שכולל את מצב הגלילה של LazyColumn או LazyRow, באמצעות rememberSaveable. הוא משתמש בLazyListState.Saver, שזה שומר מסך מותאם אישית שיכול לשמור ולשחזר את מצב הגלילה. אחרי יצירה מחדש של פעילות או תהליך (לדוגמה, אחרי שינוי בהגדרות כמו שינוי כיוון המכשיר), מצב הגלילה נשמר.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

שיטה מומלצת

rememberSaveable משתמש ב-Bundle כדי לאחסן את מצב ממשק המשתמש, שמשותף לממשקי API אחרים שגם כותבים אליו, כמו קריאות ל-onSaveInstanceState() בפעילות. עם זאת, הגודל של Bundle מוגבל, ואחסון של אובייקטים גדולים עלול להוביל לחריגות של TransactionTooLarge בזמן הריצה. הבעיה הזו יכולה להיות חמורה במיוחד באפליקציות Activity יחידות שבהן נעשה שימוש באותו Bundle בכל האפליקציה.

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

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

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

אימות שחזור המצב

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

לוגיקה עסקית

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

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

עם זאת, מופע ViewModel לא שורד את סיום התהליך שמתחיל על ידי המערכת. כדי שמצב ממשק המשתמש יישמר, צריך להשתמש במודול Saved State ל-ViewModel, שמכיל את ה-API‏ SavedStateHandle.

שיטה מומלצת

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

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

ממשקי API של SavedStateHandle

ל-SavedStateHandle יש ממשקי API שונים לאחסון מצב של רכיבי ממשק משתמש, בעיקר:

כתיבת הודעה State saveable()
StateFlow getStateFlow()

כתיבת הודעה State

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

ממשק saveable API תומך בסוגים פרימיטיביים מחוץ לקופסה ומקבל פרמטר stateSaver לשימוש ב-savers בהתאמה אישית, בדיוק כמו rememberSaveable().

בקטע הקוד הבא, message מאחסן את קלט המשתמש שהוקלד ב-TextField:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

מידע נוסף על השימוש ב-saveable API זמין במאמרי העזרה של SavedStateHandle.

StateFlow

משתמשים ב-getStateFlow() כדי לשמור את מצב רכיב ממשק המשתמש ולהשתמש בו כ-Flow מ-SavedStateHandle. ה-StateFlow הוא לקריאה בלבד, וב-API צריך לציין מפתח כדי להחליף את ה-Flow ולשלוח ערך חדש. באמצעות המפתח שהגדרתם, אתם יכולים לאחזר את StateFlow ולאסוף את הערך האחרון.

בדוגמה הבאה, savedFilterType הוא משתנה StateFlow שמאחסן סוג של מסנן שמוחל על רשימה של ערוצי צ'אט באפליקציית צ'אט:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

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

מידע נוסף על getStateFlow() API זמין במאמרי העזרה של SavedStateHandle.

סיכום

בטבלה הבאה מפורטים ממשקי ה-API שמוסברים בקטע הזה, ומתי כדאי להשתמש בכל אחד מהם כדי לשמור את מצב ממשק המשתמש:

אירוע הלוגיקה של ממשק המשתמש לוגיקה עסקית בViewModel
שינויים בהגדרות rememberSaveable אוטומטי
השבתת תהליך שהופעל על ידי המערכת rememberSaveable SavedStateHandle

ממשק ה-API שבו צריך להשתמש תלוי במיקום של המצב ובסוג הלוגיקה שנדרשת. כדי לשמור מצב שמשמש ללוגיקת ממשק משתמש, משתמשים ב-rememberSaveable. כדי לשמור מצב שמשמש ללוגיקה עסקית, אם הוא נשמר ב-ViewModel, משתמשים ב-SavedStateHandle.

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

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