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

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

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

לוגיקה עסקית

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

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

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

השיטה המומלצת

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

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

ממשקי API של SavedStateHandle

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

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

סיכום

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

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

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

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

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