שימוש במצב Compose מחוץ לממשק המשתמש

‫State<T> ו-MutableState<T> ב-Jetpack Compose הם מאגרי ערכים למטרות כלליות, שמגובים על ידי מערכת תמונת המצב של Compose. למרות שהם משמשים בעיקר להרכבה מחדש של ממשק המשתמש, מערכת תמונת המצב עצמה לא קשורה לעיבוד של ממשק המשתמש.

בעזרת הספריות של AppState, ‏Transform ו-StateStore, אתם יכולים לשמור, להפיק, לראות ולשמר את State של Compose ב-ViewModels, במנהלים, במאגרים ובשירותים שפועלים ברקע.

למה כדאי להשתמש ב-Compose State מעבר לממשק המשתמש?

כשקוראים את State של Compose בתוך בלוק של מעקב אחרי מצב – כמו רכיב UI קומפוזבילי, transform או derivedStateOf – זמן הריצה של Compose מספק שני יתרונות מרכזיים בכל שכבה של הארכיטקטורה:

  • מעקב אוטומטי ודינמי אחרי יחסי תלות: קריאת .value של מצב (או שימוש בהקצאת נכס by) רושמת באופן אוטומטי את המצב הזה כתלות של הבלוק. מכיוון שהמעקב הוא דינמי, יחסי התלות נקבעים בזמן הריצה לפי נתיבי הקוד שמופעלים. אם לא נכנסים להסתעפות מותנית (לדוגמה, אחרי בדיקת if או return מוקדם), המצבים בתוך ההסתעפות הזו לא נקראים ושינויים בהם לא מפעילים חישובים מחדש מיותרים.
  • עקביות ורמת פירוט: אתם יכולים להצהיר על מאפייני MutableState נפרדים ומפורטים, ולעטוף כתיבה של כמה מאפיינים ב-Snapshot.withMutableSnapshot. עדכוני ביניים מבודדים לתמונת המצב הניתנת לשינוי, והם מיושמים באופן אטומי כשהבלוק מסתיים. לכן, משתמשים שקוראים בתוך תמונת מצב אף פעם לא רואים מצב ביניים לא עקבי, והם מחשבים מחדש רק כשהמאפיינים הספציפיים שהם קוראים משתנים. בניגוד ל-StateFlow, אתם לא צריכים לבחור בין StateFlow<UiState> מונוליטי יחיד (שמבטל כל אוסף כששדה כלשהו משתנה) לבין מאפייני StateFlow נפרדים (שלא יכולים להתעדכן יחד בטרנזקציה אטומית אחת).

בחירת ה-API הנכון למצב בארכיטקטורה שלכם

כדי לבחור איך לשמור את State של Compose, צריך להחליט כמה זמן המצב צריך להימשך ולאילו רכיבים צריכה להיות גישה אליו:

קטגוריית המדינה משך החיים וההיקף דוגמאות ממשקי API מומלצים של Compose State
מצב פעיל הנתונים תמיד מאוחסנים בזיכרון, נשלפים מחדש בזמן ההפעלה ולא נשמרים אחרי השבתת התהליך. סטטוס החיבור לרשת, סטטוס החיבור ל-Bluetooth, מצב זמני של ממשק המשתמש כמו בחירת פריט.
מצב זמני של ממשק המשתמש הנתונים נשמרים גם אחרי שינויים בהגדרות וגם אחרי שהמערכת משביתה את התהליך, אבל לא נשמרים ללא הגבלת זמן. הם מוגבלים למסך אחד או לפעילות אחת. רכיבים מורחבים או מכווצים בממשק המשתמש, מיקום הגלילה, שאילתת החיפוש במסך או מתגי הסינון.
מצב סשן זמני הנתונים שורדים השבתת תהליך ברקע, לא נשמרים ללא הגבלת זמן; משותפים בין פעילויות, שירותים או ווידג'טים. טיימר פעיל של מצב ריכוז, עגלת קניות עם תהליך תשלום בכמה שלבים או טיוטת טופס בתהליך מילוי.
  • StateStore מוקפסל במנהל או במאגר
  • מפתחות שמטמיעים את PersistToDataStore ומסונכרנים באמצעות syncToDataStore
מצב קבוע נשמרים בדיסק ללא הגבלת זמן. העדפות והגדרות של המשתמש (DataStore), נתוני אפליקציה מובְנים וספריות מדיה (Room). ‫DataStore או תהליכי Room שנאספו בתוך transform באמצעות collectAsState

שימוש ב-Compose State ב-ViewModel

בתוך ViewModel, אתם יכולים להצהיר על קלט מסך עצמאי כמאפייני MutableState מפורטים, לשמור אותם במקרה של השבתת תהליך באמצעות SavedStateHandle.saveable, לעדכן כמה מאפיינים באופן אטומי באמצעות Snapshot.withMutableSnapshot ולשלב אותם עם זרמי Flow של מאגר בתוך transform:

sealed interface TaskListState {
    data object Loading : TaskListState
    data class Success(
        val tasks: List<Task>,
        val selectedTaskIds: Set<String>,
        val isOnline: Boolean = true,
    ) : TaskListState
}

@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set

    fun updateSearchQuery(query: String) {
        Snapshot.withMutableSnapshot {
            searchQuery = query
            selectedTaskIds = emptySet()
        }
    }

    val tasksState: State<TaskListState> = transform<TaskListState>(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading

        TaskListState.Success(
            tasks = tasks,
            selectedTaskIds = selectedTaskIds,
        )
    }
}

הדפוסים העיקריים בViewModel:

  • שימוש ב-SavedStateHandle.saveable למצב שמור ריאקטיבי: העברת searchQuery ל-SavedStateHandle.saveable עם mutableStateOf (מ-androidx.lifecycle:lifecycle-viewmodel-compose) שומרת את הערך במקרה של השבתת תהליך בתור MutableState, כך ש-transform עוקב אחרי קריאות באופן אוטומטי. הימנעו משימוש ב-SavedStateHandle.saved בלבד, שמחזיר מאפיין Kotlin פשוט במקום State של Compose, ולא מפעיל הרכבה מחדש כשהוא משתנה.
  • כתיבה אטומית של כמה מאפיינים באמצעות Snapshot.withMutableSnapshot: עדכון של searchQuery ו-selectedTaskIds בתוך Snapshot.withMutableSnapshot מבודד את הכתיבה ומבצע את שני השינויים בטרנזקציה אחת של תמונת מצב אטומית, כך שהצופים (כולל transform) רואים את שני העדכונים יחד במעבר אחד של הרכבה מחדש, במקום לראות את searchQuery החדש בשילוב עם selectedTaskIds הישן.
  • שימוש חוזר בתהליכים בין קומפוזיציות מחדש באמצעות remember(key): כשעוטפים את tasksRepository.getTasks(searchQuery) ב-remember עם searchQuery כמפתח, מבטיחים שכאשר selectedTaskIds משתנה ויוצר מחדש את הבלוק transform, Compose משתמש מחדש ב-Flow הקיים של מסד הנתונים במקום להפעיל מחדש את השאילתה.

שיתוף מצב המערכת בזמן אמת באפליקציה

במצב מערכת בזמן אמת שמשותף בין כמה ViewModels, פעילויות או שירותים – כמו סטטוס החיבור לרשת או ל-Bluetooth – לא כדאי לשמור סטטוס לא עדכני בדיסק במקרה של השבתת תהליך. במקום זאת, צריך לשלוח שאילתה לגבי מצב המערכת הנוכחי בזמן ההפעלה, לשמור אותו במאפיין mutableStateOf פרטי בתוך סינגלטון ברמת האפליקציה, ולחשוף State<T> לקריאה בלבד:

interface NetworkMonitor {
    val isOnline: State<Boolean>
}

@Singleton
class LiveNetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) : NetworkMonitor {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)

    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    override val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities,
        ) {
            _isOnline.value = networkCapabilities.hasCapability(
                NetworkCapabilities.NET_CAPABILITY_INTERNET
            )
        }

        override fun onLost(network: Network) {
            _isOnline.value = false
        }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }

    private fun checkCurrentConnectivity(): Boolean {
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities =
            connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(
            NetworkCapabilities.NET_CAPABILITY_INTERNET
        )
    }
}

‫MutableState תומך בקריאות ובעדכונים עקביים מכל שרשור, כך ש-NetworkCallback יכול לעדכן את _isOnline.value ישירות משרשור הקריאה החוזרת (callback) ברקע. אם קריאה חוזרת ברקע מעדכנת כמה מאפיינים של MutableState ביחד, צריך לעטוף את פעולות הכתיבה ב-Snapshot.withMutableSnapshot כדי שהן יחולו באופן אטומי.

כל רכיב באפליקציה יכול לקרוא את networkMonitor.isOnline.value – בין אם כבדיקה חד-פעמית, בתוך רכיב UI קומפוזבילי, או בתוך בלוק של מעקב אחרי ערך דינמי כמו listener,‏ derivedStateOf או transform. לדוגמה, TaskListViewModel יכול לקרוא את networkMonitor.isOnline.value בתוך הבלוק transform שלו:

val tasksState: State<TaskListState> = transform<TaskListState>(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(
        tasks = tasks,
        selectedTaskIds = selectedTaskIds,
        isOnline = networkMonitor.isOnline.value,
    )
}

‫transform עוקב אחרי networkMonitor.isOnline.value לצד searchQuery, selectedTaskIds והמאגר Flow, ולכן כל שינוי בקישוריות מחשב מחדש באופן אוטומטי את TaskListState.Success בלי לשלוח שאילתה מחדש למסד הנתונים.