W aplikacji Compose miejsce, w którym umieszczasz stan interfejsu, zależy od tego, czy wymaga tego logika interfejsu czy logika biznesowa. W tym dokumencie opisujemy te 2 główne scenariusze.
Sprawdzona metoda
Stan interfejsu należy umieścić w najbliższym wspólnym przodku wszystkich funkcji kompozycyjnych, które go odczytują i zapisują. Stan powinien znajdować się jak najbliżej miejsca, w którym jest używany. Właściciel stanu powinien udostępniać użytkownikom niezmienny stan i zdarzenia, które pozwalają go modyfikować.
Najbliższy wspólny przodek może też znajdować się poza kompozycją. Na przykład gdy umieszczasz stan w ViewModel, ponieważ jest on powiązany z logiką biznesową.
Na tej stronie znajdziesz szczegółowe informacje o tej sprawdzonej metodzie oraz o kwestii, o której należy pamiętać.
Typy stanu interfejsu i logiki interfejsu
Poniżej znajdziesz definicje typów stanu interfejsu i logiki, które są używane w tym dokumencie.
Stan interfejsu
Stan interfejsu to właściwość, która opisuje interfejs. Istnieją 2 typy stanu interfejsu:
- Stan interfejsu ekranu to to, co musisz wyświetlić na ekranie. Na przykład klasa
NewsUiStatemoże zawierać artykuły i inne informacje potrzebne do renderowania interfejsu. Ten stan jest zwykle powiązany z innymi warstwami hierarchii, ponieważ zawiera dane aplikacji. - Stan elementu interfejsu odnosi się do właściwości wewnętrznych elementów interfejsu, które wpływają na sposób ich renderowania. Element interfejsu może być widoczny lub ukryty oraz może mieć określony krój, rozmiar i kolor czcionki. W Jetpack Compose stan jest zewnętrzny w stosunku do funkcji kompozycyjnej. Możesz go nawet umieścić poza bezpośrednim sąsiedztwem funkcji kompozycyjnej, w funkcji kompozycyjnej wywołującej lub w kontenerze stanu. Przykładem jest
ScaffoldStatedlaScaffoldfunkcji kompozycyjnej.
Operatory logiczne
Logika w aplikacji może być logiką biznesową lub logiką interfejsu:
- Logika biznesowa to implementacja wymagań dotyczących danych aplikacji. Na przykład dodawanie artykułu do zakładek w aplikacji do czytania wiadomości, gdy użytkownik kliknie przycisk. Ta logika zapisywania zakładki w pliku lub bazie danych jest zwykle umieszczana w warstwach domeny lub danych. Kontener stanu zwykle przekazuje tę logikę do tych warstw, wywołując udostępniane przez nie metody.
- Logika interfejsu jest związana z tym, jak wyświetlać stan interfejsu na ekranie. Na przykład uzyskiwanie odpowiedniej podpowiedzi w pasku wyszukiwania, gdy użytkownik wybierze kategorię, przewijanie do określonego elementu na liście lub logika nawigacji do określonego ekranu, gdy użytkownik kliknie przycisk.
Logika interfejsu
Gdy logika interfejsu musi odczytywać lub zapisywać stan, należy ograniczyć zakres stanu do interfejsu, zgodnie z jego cyklem życia. Aby to osiągnąć, umieść stan na odpowiednim poziomie w funkcji typu „composable”. Możesz też to zrobić w zwykłej klasie zmiennej stanu, która również jest ograniczona do cyklu życia interfejsu.
Poniżej znajdziesz opis obu rozwiązań i wyjaśnienie, kiedy należy użyć którego z nich.
Funkcje kompozycyjne jako właściciel stanu
Umieszczenie logiki interfejsu i stanu elementu interfejsu w funkcjach kompozycyjnych to dobre rozwiązanie, jeśli stan i logika są proste. Możesz pozostawić stan wewnętrzny w funkcji kompozycyjnej lub umieścić go w innym miejscu, jeśli jest to wymagane.
Nie trzeba przenosić stanu
Nie zawsze trzeba umieszczać stanu w innym miejscu. Stan może być przechowywany wewnętrznie w funkcji kompozycyjnej, jeśli żadna inna funkcja kompozycyjna nie musi go kontrolować. W tym fragmencie kodu znajduje się funkcja kompozycyjna, która rozwija się i zwija po kliknięciu:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state Text( text = AnnotatedString(message.content), modifier = Modifier.clickable { showDetails = !showDetails // Apply UI logic } ) if (showDetails) { Text(message.timestamp) } }
Zmienna showDetails to stan wewnętrzny tego elementu interfejsu. Jest ona odczytywana i modyfikowana tylko w tej funkcji kompozycyjnej, a logika do niej stosowana jest bardzo prosta.
Umieszczenie stanu w innym miejscu w tym przypadku nie przyniosłoby wielu korzyści, więc możesz pozostawić go wewnętrznie. Dzięki temu ta funkcja kompozycyjna jest właścicielem i jedynym źródłem informacji o stanie rozwiniętym.
Umieszczanie stanu w innym miejscu w funkcjach kompozycyjnych
Jeśli musisz udostępnić stan elementu interfejsu innym funkcjom kompozycyjnym i stosować do niego logikę interfejsu w różnych miejscach, możesz umieścić go wyżej w hierarchii interfejsu. Dzięki temu funkcje kompozycyjne są bardziej wielokrotnego użytku i łatwiejsze do testowania.
Poniższy przykład to aplikacja do czatu, która implementuje 2 funkcje:
- Przycisk
JumpToBottomprzewija listę wiadomości na dół. Przycisk wykonuje logikę interfejsu na stanie listy. - Lista
MessagesListprzewija się na dół po wysłaniu przez użytkownika nowych wiadomości. UserInput wykonuje logikę interfejsu na stanie listy.
JumpToBottom i przewijaniem na dół po otrzymaniu nowych wiadomościHierarchia funkcji kompozycyjnych wygląda tak:
Stan LazyColumn jest umieszczony na ekranie rozmowy, dzięki czemu aplikacja może wykonywać logikę interfejsu i odczytywać stan ze wszystkich funkcji kompozycyjnych, które tego wymagają:
LazyColumn z LazyColumn w ConversationScreenOstatecznie funkcje kompozycyjne wyglądają tak:
LazyListState umieszczonym w ConversationScreenKod wygląda tak:
@Composable private fun ConversationScreen(/*...*/) { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen MessagesList(messages, lazyListState) // Reuse same state in MessageList UserInput( onMessageSent = { // Apply UI logic to lazyListState scope.launch { lazyListState.scrollToItem(0) } }, ) } @Composable private fun MessagesList( messages: List<Message>, lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value ) { LazyColumn( state = lazyListState // Pass hoisted state to LazyColumn ) { items(messages, key = { message -> message.id }) { item -> Message(/*...*/) } } val scope = rememberCoroutineScope() JumpToBottom(onClicked = { scope.launch { lazyListState.scrollToItem(0) // UI logic being applied to lazyListState } }) }
LazyListState jest umieszczony tak wysoko, jak to konieczne w przypadku logiki interfejsu, która ma być stosowana. Ponieważ jest on inicjowany w funkcji typu „composable”, jest przechowywany w kompozycji zgodnie z jej cyklem życia.
Zwróć uwagę, że lazyListState jest zdefiniowany w metodzie MessagesList z wartością domyślną rememberLazyListState(). Jest to typowy wzorzec w Compose.
Dzięki temu funkcje kompozycyjne są bardziej wielokrotnego użytku i elastyczne. Możesz wtedy używać funkcji kompozycyjnej w różnych częściach aplikacji, które nie muszą kontrolować stanu. Zwykle tak jest podczas testowania lub wyświetlania podglądu funkcji kompozycyjnej. W ten sposób LazyColumn definiuje swój stan.
LazyListState to ConversationScreenZwykła klasa kontenera stanu jako właściciel stanu
Gdy funkcja kompozycyjna zawiera złożoną logikę interfejsu, która obejmuje co najmniej 1 pole stanu elementu interfejsu, powinna przekazać tę odpowiedzialność kontenerom stanu, takim jak zwykła klasa kontenera stanu. Dzięki temu logika funkcji kompozycyjnej jest łatwiejsza do testowania w izolacji i mniej złożona. To podejście sprzyja zasadzie rozdzielenia odpowiedzialności: funkcja kompozycyjna odpowiada za emitowanie elementów interfejsu, a kontener stanu zawiera logikę interfejsu i stan elementu interfejsu.
Zwykłe klasy zmiennej stanu udostępniają wygodne funkcje elementom wywołującym funkcję typu „composable”, dzięki czemu nie muszą oni sami pisać tej logiki.
Te zwykłe klasy są tworzone i zapamiętywane w kompozycji. Ponieważ są one
zgodne z cyklem życia funkcji kompozycyjnej, mogą przyjmować typy udostępniane przez
bibliotekę Compose, takie jak rememberNavController() czy rememberLazyListState().
Przykładem jest LazyListState zwykła klasa kontenera stanu, zaimplementowana w Compose w celu kontrolowania złożoności interfejsu LazyColumn
lub LazyRow.
// LazyListState.kt @Stable class LazyListState constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0 ) : ScrollableState { /** * The holder class for the current scroll position. */ private val scrollPosition = LazyListScrollPosition( firstVisibleItemIndex, firstVisibleItemScrollOffset ) suspend fun scrollToItem(/*...*/) { /*...*/ } override suspend fun scroll() { /*...*/ } suspend fun animateScrollToItem() { /*...*/ } }
LazyListState hermetyzuje stan LazyColumn, przechowując
scrollPosition dla tego elementu interfejsu. Udostępnia też metody modyfikowania pozycji przewijania, np. przewijania do danego elementu.
Jak widać, zwiększanie odpowiedzialności funkcji kompozycyjnej zwiększa potrzebę użycia kontenera stanu. Odpowiedzialność może dotyczyć logiki interfejsu lub po prostu ilości stanu, który trzeba śledzić.
Innym typowym wzorcem jest używanie zwykłej klasy zmiennej stanu do obsługi złożoności funkcji typu „composable” na poziomie głównym w aplikacji. Możesz użyć takiej klasy do hermetyzowania stanu na poziomie aplikacji, takiego jak stan Navigation i rozmiar ekranu. Pełny opis znajdziesz na stronie Logika interfejsu i jej kontener stanu.
Logika biznesowa
Jeśli funkcje kompozycyjne i zwykłe klasy kontenera stanu odpowiadają za logikę interfejsu i stan elementu interfejsu, kontener stanu na poziomie ekranu odpowiada za te zadania:
- Udostępnianie dostępu do logiki biznesowej aplikacji, która zwykle znajduje się w innych warstwach hierarchii, takich jak warstwy biznesowa i danych.
- Przygotowywanie danych aplikacji do prezentacji na konkretnym ekranie, który staje się stanem interfejsu ekranu.
ViewModel jako właściciel stanu
Gdy umieszczasz stan interfejsu w ViewModel, przenosisz go poza kompozycję.
ViewModel jest przechowywany poza kompozycją.ViewModel nie są przechowywane jako część kompozycji. Są one udostępniane przez
framework i są ograniczone do ViewModelStoreOwner, którym może być
aktywność, fragment, wykres nawigacji lub miejsce docelowe wykresu nawigacji. Więcej
informacji o ViewModel zakresach znajdziesz w dokumentacji.
ViewModel jest wtedy źródłem informacji i najbliższym wspólnym elementem nadrzędnym stanu interfejsu.
Stan interfejsu ekranu
Zgodnie z powyższymi definicjami stan interfejsu ekranu jest tworzony przez zastosowanie reguł biznesowych. Ponieważ kontener stanu na poziomie ekranu jest za to odpowiedzialny, oznacza to, że stan interfejsu ekranu jest zwykle umieszczany w kontenerze stanu na poziomie ekranu, w tym przypadku w ViewModel.
Rozważmy ConversationViewModel aplikacji do czatu i sposób, w jaki udostępnia ona stan interfejsu ekranu oraz zdarzenia, które pozwalają go modyfikować:
class ConversationViewModel( channelId: String, messagesRepository: MessagesRepository ) : ViewModel() { val messages = messagesRepository .getLatestMessages(channelId) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) // Business logic fun sendMessage(message: Message) { /* ... */ } }
Funkcje kompozycyjne używają stanu interfejsu ekranu umieszczonego w ViewModel. Aby zapewnić dostęp do logiki biznesowej, wstrzyknij instancję ViewModel do funkcji kompozycyjnych na poziomie ekranu.
Poniżej znajdziesz przykład ViewModel używanego w funkcji kompozycyjnej na poziomie ekranu.
W tym przypadku funkcja kompozycyjna ConversationScreen() używa stanu interfejsu ekranu umieszczonego w ViewModel:
@Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val messages by conversationViewModel.messages.collectAsStateWithLifecycle() ConversationScreen( messages = messages, onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) } ) } @Composable private fun ConversationScreen( messages: List<Message>, onSendMessage: (Message) -> Unit ) { MessagesList(messages, onSendMessage) /* ... */ }
Przekazywanie właściwości
„Przekazywanie właściwości” odnosi się do przekazywania danych przez kilka zagnieżdżonych komponentów podrzędnych do miejsca, w którym są odczytywane.
Typowym przykładem, w którym może wystąpić przekazywanie właściwości w Compose, jest wstrzykiwanie zmiennej stanu na poziomie ekranu na najwyższym poziomie i przekazywanie stanu oraz zdarzeń do funkcji kompozycyjnych podrzędnych. Może to dodatkowo spowodować przeciążenie sygnatur funkcji kompozycyjnych.
Chociaż udostępnianie zdarzeń jako poszczególnych parametrów lambda może przeciążyć sygnaturę funkcji, maksymalizuje to widoczność obowiązków funkcji typu „composable”. Możesz od razu zobaczyć, co robi.
Przekazywanie właściwości jest lepsze niż tworzenie klas opakowujących, które hermetyzują stan i zdarzenia w jednym miejscu, ponieważ zmniejsza to widoczność obowiązków funkcji kompozycyjnej. Dzięki temu, że nie masz klas opakowujących, z większym prawdopodobieństwem będziesz przekazywać do funkcji kompozycyjnych tylko te parametry, których potrzebują, co jest sprawdzoną metodą.
Ta sama sprawdzona metoda obowiązuje, jeśli te zdarzenia są zdarzeniami nawigacji. Możesz dowiedzieć się więcej na ten temat w dokumentacji nawigacji.
Jeśli stwierdzisz problem z wydajnością, możesz też odłożyć odczyt stanu. Więcej informacji znajdziesz w dokumentacji dotyczącej wydajności.
Stan elementu interfejsu
Stan elementu interfejsu możesz umieścić w zmiennej stanu na poziomie ekranu, jeśli istnieje logika biznesowa, która musi go odczytywać lub zapisywać.
Kontynuując przykład aplikacji do czatu, aplikacja wyświetla sugestie użytkowników w
czacie grupowym, gdy użytkownik wpisze @ i podpowiedź. Te sugestie pochodzą z warstwy danych, a logika obliczania listy sugestii użytkowników jest uważana za logikę biznesową. Ta funkcja wygląda tak:
@ i podpowiedźViewModel implementujący tę funkcję wyglądałby tak:
class ConversationViewModel(/*...*/) : ViewModel() { // Hoisted state var inputMessage by mutableStateOf("") private set val suggestions: StateFlow<List<Suggestion>> = snapshotFlow { inputMessage } .filter { hasSocialHandleHint(it) } .mapLatest { getHandle(it) } .mapLatest { repository.getSuggestions(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyList() ) fun updateInput(newInput: String) { inputMessage = newInput } }
inputMessage to zmienna przechowująca stan TextField. Za każdym razem, gdy użytkownik wpisze nowe dane, aplikacja wywołuje logikę biznesową, aby utworzyć suggestions.
suggestions to stan interfejsu ekranu, który jest używany w interfejsie Compose przez zbieranie danych z StateFlow.
Caveat
W przypadku niektórych stanów elementów interfejsu Compose umieszczanie ich w ViewModel może wymagać szczególnych rozważań. Na przykład niektóre kontenery stanu elementów interfejsu Compose udostępniają metody modyfikowania stanu. Niektóre z nich mogą być funkcjami zawieszającymi, które wywołują animacje. Te funkcje zawieszające mogą zgłaszać wyjątki, jeśli wywołasz
je z CoroutineScope, który nie jest ograniczony do
kompozycji.
Załóżmy, że zawartość szuflady aplikacji jest dynamiczna i musisz ją pobrać i odświeżyć z warstwy danych po jej zamknięciu. Stan szuflady należy umieścić w ViewModel, aby można było wywoływać logikę interfejsu i logikę biznesową tego elementu z poziomu właściciela stanu.
Jednak wywołanie metody DrawerState close() za pomocą
viewModelScope z interfejsu Compose powoduje wyjątek środowiska wykonawczego typu
IllegalStateException z komunikatem „a
MonotonicFrameClock is not available in this
CoroutineContext”.
Aby to naprawić, użyj CoroutineScope ograniczonego do kompozycji. Udostępnia on MonotonicFrameClock w CoroutineContext, który jest niezbędny do działania funkcji zawieszających.
Aby naprawić tę awarię, zmień CoroutineContext współprogramu w ViewModel na taki, który jest ograniczony do kompozycji. Może to wyglądać tak:
class ConversationViewModel(/*...*/) : ViewModel() { val drawerState = DrawerState(initialValue = DrawerValue.Closed) private val _drawerContent = MutableStateFlow(DrawerContent.Empty) val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow() fun closeDrawer(uiScope: CoroutineScope) { viewModelScope.launch { withContext(uiScope.coroutineContext) { // Use instead of the default context drawerState.close() } // Fetch drawer content and update state _drawerContent.update { content } } } } // in Compose @Composable private fun ConversationScreen( conversationViewModel: ConversationViewModel = viewModel() ) { val scope = rememberCoroutineScope() ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) }) }
Więcej informacji
Więcej informacji o stanie i Jetpack Compose znajdziesz w tych dodatkowych materiałach.
Przykłady
Codelabs
Filmy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Zapisywanie stanu interfejsu w Compose
- Listy i siatki
- Projektowanie interfejsu Compose