Używanie stanu tworzenia wiadomości poza interfejsem

State<T> i MutableState<T> w Jetpack Compose to ogólne obiekty przechowujące wartości, które są obsługiwane przez system zrzutów Compose. Chociaż najczęściej służą do rekompozycji interfejsu, sam system migawek nie jest powiązany z renderowaniem interfejsu.

Dzięki bibliotekom AppState – Transform i StateStore – możesz przechowywać, uzyskiwać, obserwować i utrzymywać State Compose w modelach widoku, menedżerach, repozytoriach i usługach w tle.

Dlaczego warto używać stanu Compose poza interfejsem?

Gdy odczytujesz stan Compose State w bloku obserwującym stan, takim jak funkcja kompozycyjna interfejsu, transform lub derivedStateOf, środowisko wykonawcze Compose zapewnia 2 kluczowe korzyści na dowolnej warstwie architektury:

  • Automatyczne i dynamiczne śledzenie zależności: odczytanie .value stanu (lub użycie delegowania właściwości by) automatycznie rejestruje ten stan jako zależność bloku. Śledzenie jest dynamiczne, więc zależności są określane w czasie działania przez ścieżki kodu, które są wykonywane. Jeśli gałąź warunkowa nie zostanie wykonana (np. po sprawdzeniu if lub wczesnym return), stany w tej gałęzi nie zostaną odczytane, a zmiany w nich nie spowodują niepotrzebnych ponownych obliczeń.
  • Spójność i szczegółowość: możesz zadeklarować osobne, szczegółowe właściwości MutableState i opakować zapisy wielu właściwości w Snapshot.withMutableSnapshot. Aktualizacje pośrednie są izolowane w modyfikowalnym zrzucie i stosowane niepodzielnie po zakończeniu bloku, dzięki czemu obserwatorzy odczytujący dane w zrzucie nigdy nie widzą niespójnego stanu pośredniego i ponownie obliczają dane tylko wtedy, gdy zmieniają się określone właściwości, które odczytują. W odróżnieniu od StateFlow nie musisz wybierać między pojedynczym monolitycznym StateFlow<UiState> (który unieważnia każdy kolektor, gdy zmieni się dowolne pole) a oddzielnymi właściwościami StateFlow (których nie można aktualizować razem w ramach jednej transakcji niepodzielnej).

Wybieranie odpowiedniego interfejsu API stanu w architekturze

Wybierz sposób przechowywania i utrzymywania stanu State w Compose na podstawie tego, jak długo dany stan ma być aktywny i które komponenty muszą mieć do niego dostęp:

Kategoria stanu Czas życia i zakres Przykłady Zalecane interfejsy API Compose State
Stan aktywny Zawsze przechowywane w pamięci; pobierane na nowo podczas uruchamiania i nie zachowywane po śmierci procesu. Stan połączenia sieciowego, stan połączenia Bluetooth, tymczasowy stan interfejsu, np. wybór elementu.
Tymczasowy stan interfejsu Przetrwa zmiany konfiguracji i śmierć procesu zainicjowaną przez system, nie jest przechowywany bezterminowo; ograniczony do jednego ekranu lub aktywności. rozwinięte lub zwinięte elementy interfejsu, pozycję przewijania, zapytanie wyszukiwania na ekranie lub przełączniki filtrów;
Stan sesji tymczasowej Przetrwa śmierć procesu w tle, nie jest przechowywany bezterminowo; udostępniany między aktywnościami, usługami lub widżetami. aktywny minutnik, wieloetapowy proces płatności lub wersja robocza formularza w trakcie wypełniania;
  • StateStore umieszczone w menedżerze lub repozytorium
  • Klucze implementujące PersistToDataStore i synchronizowane za pomocą syncToDataStore
Stan stały Są przechowywane na dysku bez ograniczeń czasowych. ustawienia i preferencje użytkownika (DataStore), uporządkowane dane aplikacji i biblioteki multimediów (Room). collectAsState zbiera przepływy DataStore lub Room w transform

Używanie stanu Compose w ViewModel

Wewnątrz ViewModel możesz zadeklarować niezależne dane wejściowe ekranu jako szczegółowe właściwości MutableState, zachować je po śmierci procesu za pomocą SavedStateHandle.saveable, zaktualizować wiele właściwości niepodzielnie za pomocą Snapshot.withMutableSnapshot i połączyć je ze strumieniami repozytorium Flow wewnątrz 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,
        )
    }
}

Kluczowe wzorce w tym pliku (ViewModel):

  • Używanie SavedStateHandle.saveable do reaktywnego zapisanego stanu: przekazywanie searchQuery do SavedStateHandle.saveable za pomocą mutableStateOf (z androidx.lifecycle:lifecycle-viewmodel-compose) zachowuje wartość po śmierci procesu jako MutableState, więc transform automatycznie śledzi odczyty. Unikaj zwykłego SavedStateHandle.saved, które zwraca zwykłą właściwość Kotlin, a nie Compose State, i nie powoduje rekompozycji po zmianie.
  • Niepodzielne zapisy wielu właściwości za pomocą Snapshot.withMutableSnapshot: aktualizowanie searchQuery i selectedTaskIds wewnątrz Snapshot.withMutableSnapshot izoluje zapisy i zatwierdza obie zmiany w jednej niepodzielnej transakcji migawki, dzięki czemu obserwatorzy (w tym transform) widzą obie aktualizacje razem w jednym przebiegu rekompozycji, a nie nowy searchQuery sparowany z nieaktualnym selectedTaskIds.
  • Ponowne używanie przepływów w rekompozycjach za pomocą remember(key): opakowanie tasksRepository.getTasks(searchQuery) w remember z searchQuery jako kluczem gwarantuje, że gdy selectedTaskIds zmieni się i ponownie skomponuje blok transform, Compose ponownie użyje istniejącego przepływu bazy danych Flow, zamiast ponownie uruchamiać zapytanie.

Udostępnianie stanu systemu na żywo w aplikacji

W przypadku stanu systemu na żywo udostępnianego w wielu modelach ViewModel, aktywnościach lub usługach – takich jak stan połączenia sieciowego lub Bluetooth – nie chcesz zapisywać nieaktualnego stanu na dysku po śmierci procesu. Zamiast tego podczas uruchamiania wyślij zapytanie o bieżący stan systemu, zachowaj go we właściwości prywatnej mutableStateOf w singletonie o zakresie aplikacji i udostępnij właściwość tylko do odczytu 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 obsługuje spójne odczyty i aktualizacje z dowolnego wątku, więc NetworkCallback może aktualizować _isOnline.value bezpośrednio z wątku wywołania zwrotnego w tle. Jeśli wywołanie zwrotne w tle aktualizuje jednocześnie kilka właściwości MutableState, umieść zapisy w Snapshot.withMutableSnapshot, aby były stosowane niepodzielnie.

Każdy komponent w aplikacji może odczytywać networkMonitor.isOnline.value – zarówno w ramach jednorazowego sprawdzenia, jak i w ramach funkcji kompozycyjnej interfejsu lub bloku obserwującego stan, takiego jak listener, derivedStateOf lub transform. Na przykład TaskListViewModel może odczytać networkMonitor.isOnline.value w bloku 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,
    )
}

Ponieważ transform śledzi networkMonitor.isOnline.value wraz z searchQuery, selectedTaskIds i repozytorium Flow, każda zmiana połączenia automatycznie powoduje ponowne obliczenie TaskListState.Success bez ponownego wysyłania zapytania do bazy danych.