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
.valuestanu (lub użycie delegowania właściwościby) 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 sprawdzeniuiflub wczesnymreturn), 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
MutableStatei opakować zapisy wielu właściwości wSnapshot.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 odStateFlownie musisz wybierać między pojedynczym monolitycznymStateFlow<UiState>(który unieważnia każdy kolektor, gdy zmieni się dowolne pole) a oddzielnymi właściwościamiStateFlow(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; |
|
| 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.saveabledo reaktywnego zapisanego stanu: przekazywaniesearchQuerydoSavedStateHandle.saveableza pomocąmutableStateOf(zandroidx.lifecycle:lifecycle-viewmodel-compose) zachowuje wartość po śmierci procesu jakoMutableState, więctransformautomatycznie śledzi odczyty. Unikaj zwykłegoSavedStateHandle.saved, które zwraca zwykłą właściwość Kotlin, a nie ComposeState, i nie powoduje rekompozycji po zmianie. - Niepodzielne zapisy wielu właściwości za pomocą
Snapshot.withMutableSnapshot: aktualizowaniesearchQueryiselectedTaskIdswewnątrzSnapshot.withMutableSnapshotizoluje zapisy i zatwierdza obie zmiany w jednej niepodzielnej transakcji migawki, dzięki czemu obserwatorzy (w tymtransform) widzą obie aktualizacje razem w jednym przebiegu rekompozycji, a nie nowysearchQuerysparowany z nieaktualnymselectedTaskIds. - Ponowne używanie przepływów w rekompozycjach za pomocą
remember(key): opakowanietasksRepository.getTasks(searchQuery)wrememberzsearchQueryjako kluczem gwarantuje, że gdyselectedTaskIdszmieni się i ponownie skomponuje bloktransform, Compose ponownie użyje istniejącego przepływu bazy danychFlow, 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.