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

בהתאם למקום שבו המצב מועלה וללוגיקה הנדרשת, אתם יכולים להשתמש בממשקי 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, או הטמעה של מחלקת שמירה בהתאמה אישית שמרחיבה את המחלקה Saver של סביבת זמן הריצה של Compose. מידע נוסף על השיטות האלה זמין במאמר דרכים לאחסון מצב.

בקטע הקוד הבא, ה-API של Compose‏ rememberLazyListState מאחסן את 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 (אם יש כזה), ומצב ממשק המשתמש של המסך נוצר שוב משכבת הנתונים.

‫SavedStateHandle ממשקי API

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

כתיבה State saveable()
StateFlow getStateFlow()

כתיבת State

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

ה-API של saveable תומך בסוגים פרימיטיביים באופן מיידי ומקבל פרמטר stateSaver כדי להשתמש בשומרי מצב מותאמים אישית, בדיוק כמו 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) }
    )
}

במסמכי התיעוד של SavedStateHandle מוסבר איך משתמשים ב-API של saveable.

StateFlow

משתמשים ב-getStateFlow() כדי לאחסן את המצב של רכיב ממשק המשתמש ולצרוך אותו כזרימה מ-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 רשום לתהליך כדי לבצע את סינון הערוצים.

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

סיכום

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

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

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

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

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