Stan w Compose.
UI w Compose odzwierciedla stan. Gdy stan się zmienia, UI się aktualizuje. To sedno deklaratywnego UI. Dziś wyjaśniamy, jak działa stan w Compose i jak dobrze nim zarządzać.
W deklaratywnym UI opisujesz, jak UI ma wyglądać dla danego stanu.
Stan to dane, które mogą się zmieniać. Wartość licznika. Zawartość pola tekstowego. Informacja, czy checkbox jest zaznaczony.
Gdy stan się zmienia, UI jest budowany od nowa. Compose ponownie wywołuje twoje funkcje composable z nowym stanem. One tworzą nowy UI.
Nie aktualizujesz UI ręcznie. Zmieniasz stan. Aktualizacją zajmuje się Compose.
To daje dużą siłę. UI jest zawsze spójny ze stanem. Nie ma błędów z powodu tego, że zapomniałeś zaktualizować jakiś fragment.
Weźmy taki przykład: var count = 0 wewnątrz composable. Button zwiększa count. Text pokazuje count.
To nie działa. Zmienna jest tworzona od nowa przy każdym uruchomieniu composable. Zmiany przepadają.
Funkcje composable mogą być uruchamiane wiele razy. Przy każdej zmianie stanu Compose może ponownie wywołać twój composable. Zmienne lokalne tego nie przetrwają.
Potrzebujesz sposobu, żeby stan przetrwał kolejne recomposition. Zmienne lokalne nie wystarczą.
remember zachowuje stan między kolejnymi recomposition.
val count = remember { mutableStateOf(0) }.
remember mówi Compose: zachowaj tę wartość. Nie twórz jej od nowa podczas recomposition.
mutableStateOf tworzy obserwowalny stan. Gdy się zmieni, composable, które go odczytują, przechodzą recomposition.
Para remember i mutableStateOf to sposób na przechowywanie stanu w composable.
Wartość przetrwa recomposition. Nie przetrwa natomiast zmiany konfiguracji ani śmierci procesu (process death).
mutableStateOf tworzy obiekt przechowujący stan (state holder).
Opakowuje on wartość. Przez niego możesz ją odczytywać i zapisywać.
val state = mutableStateOf("initial"). state.value zwraca napis. state.value = "new" go zmienia.
Gdy zapisujesz wartość, Compose o tym wie. Planuje recomposition każdego composable, który odczytał ten stan.
Na tym polega reaktywność. Zmieniasz stan, a UI aktualizuje się automatycznie.
Pisanie wszędzie state.value jest rozwlekłe. Pomaga tu delegacja właściwości w Kotlinie.
var count by remember { mutableStateOf(0) }.
Teraz count zachowuje się jak zwykła zmienna. count zwraca wartość. count = 5 ją zmienia.
Pod spodem delegacja wywołuje getter i setter value. Twój kod jest jednak czytelniejszy.
Żeby używać tej składni, zaimportuj getValue i setValue z compose runtime.
Recomposition to ponowne wywołanie twoich funkcji composable przez Compose.
Następuje, gdy zmienia się stan. Compose wykrywa zmianę i ponownie uruchamia composable, których ona dotyczy.
Compose działa sprytnie. Przeprowadza recomposition tylko tych composable, które odczytują zmieniony stan. Nie całego UI.
Funkcje composable muszą być szybkie, bo uruchamiają się często. Nie wykonuj w nich ciężkiej pracy.
Efekty uboczne w composable mogą sprawiać problemy. Pamiętaj: composable mogą zostać uruchomione wiele razy.
State hoisting przenosi stan w górę, do miejsca wywołania.
Composable otrzymuje stan i callback zdarzenia. Nie jest właścicielem stanu.
@Composable fun Counter(count: Int, onIncrement: () -> Unit). Composable wyświetla count. Po kliknięciu wywołuje onIncrement.
Właścicielem stanu jest rodzic. Przekazuje count i obsługuje onIncrement.
Po co hoisting? Reużywalność. Testowalność. Composable jest bezstanowy (stateless). Działa z dowolnym źródłem stanu.
Bezstanowe composable łatwiej pokazać w Preview, przetestować i użyć ponownie.
Composable stateful sam zarządza swoim stanem.
@Composable fun Counter() { var count by remember { … }; Text("$count"); Button(onClick = { count++ }) { … } }.
Jest samowystarczalny i łatwy w użyciu. Trudniej go jednak przetestować i użyć ponownie.
Composable stateless otrzymuje stan z zewnątrz.
@Composable fun Counter(count: Int, onIncrement: () -> Unit) { … }.
Steruje nim kod wywołujący. Jest bardziej elastyczny i łatwiejszy do testowania.
Dobra praktyka: tam, gdzie się da, twórz composable bezstanowe. Przenoś stan na poziom, który go potrzebuje.
Przykład: formularz z polem tekstowym.
Właścicielem stanu jest ekran nadrzędny: var name by remember { mutableStateOf("") }.
Pole otrzymuje name i onNameChange.
@Composable fun NameInput(name: String, onNameChange: (String) -> Unit) { OutlinedTextField(value = name, onValueChange = onNameChange) }.
Rodzic umieszcza pole w swoim UI: NameInput(name, onNameChange = { name = it }).
Stan jest wyniesiony w górę (hoisted). Pole jest bezstanowe. Łatwo je pokazać w Preview, przetestować i użyć ponownie.
Podsumujmy.
Stan steruje UI w Compose. Gdy stan się zmienia, UI przechodzi recomposition.
remember zachowuje stan między kolejnymi recomposition.
mutableStateOf tworzy obserwowalny stan, który wyzwala recomposition.
State hoisting przenosi stan do kodu wywołującego, co ułatwia ponowne użycie.
Bezstanowe composable łatwiej przetestować i użyć ponownie niż stanowe.
Projektuj composable jako bezstanowe tam, gdzie ma to sens. Wynoś stan na odpowiedni poziom.