W aplikacji Compose miejsce, w którym podnosisz 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 użytkownika należy przenieść do najniższego wspólnego przodka wszystkich funkcji kompozycyjnych, które go odczytują i zapisują. Stan powinien być przechowywany jak najbliżej miejsca, w którym jest używany. Z poziomu właściciela stanu udostępniaj konsumentom niezmienny stan i zdarzenia, aby go modyfikować.
Najniższy wspólny przodek może też znajdować się poza funkcją kompozycyjną. Na przykład podczas przenoszenia stanu w ViewModel, ponieważ wymaga tego logika biznesowa.
Na tej stronie szczegółowo opisujemy tę sprawdzoną metodę i podajemy zastrzeżenie, o którym należy pamiętać.
Typy stanu i logiki interfejsu
Poniżej znajdziesz definicje typów stanu i logiki interfejsu, które są używane w tym dokumencie.
Stan UI
Stan interfejsu to właściwość, która opisuje interfejs. Istnieją 2 rodzaje stanu interfejsu:
- Stan interfejsu ekranu to to, co musisz wyświetlić na ekranie. Na przykład klasa
NewsUiStatemoże zawierać artykuły z wiadomościami i inne informacje potrzebne do renderowania interfejsu. Ten stan jest zwykle połączony 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 i może mieć określoną czcionkę, rozmiar czcionki lub kolor czcionki. W Jetpack Compose stan jest zewnętrzny względem funkcji typu „composable” i można go nawet przenieść z bezpośredniego sąsiedztwa funkcji typu „composable” do wywołującej funkcji typu „composable” lub zmiennej stanu. Przykładem jest
ScaffoldStatedla funkcji kompozycyjnejScaffold.
Operatory logiczne
Logika w aplikacji może być logiką biznesową lub logiką interfejsu:
- Logika biznesowa to implementacja wymagań dotyczących usług w odniesieniu do danych aplikacji. Na przykład dodanie artykułu do zakładek w aplikacji do czytania wiadomości, gdy użytkownik kliknie przycisk. Logika zapisywania zakładki w pliku lub bazie danych jest zwykle umieszczana w warstwach domeny lub danych. Zmienna stanu zwykle przekazuje tę logikę do tych warstw, wywołując metody, które udostępniają.
- Logika interfejsu jest związana z tym, jak wyświetlać stan interfejsu na ekranie. Na przykład uzyskanie odpowiedniej podpowiedzi na 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 odczytać lub zapisać stan, należy ograniczyć zakres stanu do interfejsu zgodnie z jego cyklem życia. Aby to osiągnąć, musisz przenieść stan na odpowiedni poziom w funkcji typu „composable”. Możesz też zrobić to 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 ich używać.
Komponenty jako właściciel stanu
Umieszczanie logiki i stanu elementów interfejsu w funkcjach kompozycyjnych jest dobrym podejściem, jeśli stan i logika są proste. Możesz pozostawić stan wewnętrzny w funkcji kompozycyjnej lub wyciągnąć go w razie potrzeby.
Nie trzeba przenosić stanu
Przenoszenie stanu nie zawsze jest wymagane. Stan może być przechowywany wewnętrznie w funkcji kompozycyjnej, gdy ż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 odczytywany i modyfikowany tylko w tej funkcji kompozycyjnej, a zastosowana do niego logika jest bardzo prosta.
W tym przypadku przeniesienie stanu nie przyniosłoby więc wielu korzyści, dlatego możesz pozostawić go jako wewnętrzny. Dzięki temu ten element kompozycyjny będzie właścicielem i jedynym źródłem wiarygodnych danych o stanie rozwinięcia.
Windowanie w funkcjach kompozycyjnych
Jeśli chcesz udostępnić stan elementu interfejsu innym elementom kompozycyjnym i zastosować do niego logikę interfejsu w różnych miejscach, możesz przenieść go wyżej w hierarchii interfejsu. Dzięki temu elementy kompozycyjne są bardziej wielokrotnego użytku i łatwiejsze do przetestowania.
Poniższy przykład to aplikacja do obsługi czatu, która implementuje 2 funkcje:
- Przycisk
JumpToBottomprzewija listę wiadomości w dół. Przycisk wykonuje logikę interfejsu na stanie listy. - Po wysłaniu przez użytkownika nowych wiadomości lista
MessagesListprzewija się do dołu. UserInput wykonuje logikę interfejsu na stanie listy.
JumpToBottom i przewijaniem do dołu przy nowych wiadomościachHierarchia funkcji kompozycyjnych wygląda tak:
Stan LazyColumn jest przenoszony na ekran rozmowy, dzięki czemu aplikacja może wykonywać logikę interfejsu i odczytywać stan ze wszystkich funkcji kompozycyjnych, które tego wymagają:
LazyColumn z LazyColumn do ConversationScreenOstatecznie elementy kompozycyjne to:
LazyListState przeniesionym do 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 podnoszony tak wysoko, jak wymaga tego logika interfejsu, którą trzeba zastosować. Ponieważ jest inicjowany w funkcji typu „composable”, jest przechowywany w kompozycji i podlega jej cyklowi życia.
Zauważ, że zmienna lazyListState jest zdefiniowana w metodzie MessagesList z wartością domyślną rememberLazyListState(). To typowy wzorzec w Compose.
Dzięki temu elementy kompozycyjne są bardziej elastyczne i można ich używać wielokrotnie. Następnie możesz używać funkcji kompozycyjnej w różnych częściach aplikacji, które mogą nie wymagać kontrolowania stanu. Zwykle dzieje się tak podczas testowania lub wyświetlania podglądu funkcji kompozycyjnej. Właśnie tak LazyColumn definiuje swój stan.
LazyListState to ConversationScreenZwykła klasa zmiennej stanu jako właściciel stanu
Gdy element kompozycyjny zawiera złożoną logikę interfejsu, która obejmuje co najmniej 1 pole stanu elementu interfejsu, powinien przekazać to zadanie zmiennym stanu, np. zwykłej klasie zmiennej stanu. Dzięki temu logika funkcji kompozycyjnej jest łatwiejsza do przetestowania w izolacji i mniej złożona. Takie podejście jest zgodne z zasadą rozdzielenia zagadnień: funkcja kompozycyjna odpowiada za emitowanie elementów interfejsu, a zmienna stanu zawiera logikę interfejsu i stan elementu interfejsu.
Zwykłe klasy zmiennych stanu udostępniają wygodne funkcje elementom wywołującym funkcję typu „composable”, dzięki czemu nie muszą one same pisać tej logiki.
Te zwykłe klasy są tworzone i zapamiętywane w kompozycji. Ponieważ są zgodne z cyklem życia funkcji kompozycyjnej, mogą przyjmować typy udostępniane przez bibliotekę Compose, takie jak rememberNavController() lub rememberLazyListState().
Przykładem jest zwykła klasa zmiennej stanu LazyListState zaimplementowana w Compose, która pozwala kontrolować złożoność 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 zawiera stan LazyColumn, w którym przechowywany jest scrollPosition dla tego elementu interfejsu. Udostępnia też metody modyfikowania pozycji przewijania, np. przez przewijanie do danego elementu.
Jak widać, zwiększenie zakresu odpowiedzialności funkcji kompozycyjnej zwiększa potrzebę użycia zmiennej stanu. Odpowiedzialność może dotyczyć logiki interfejsu lub tylko ilości stanu do śledzenia.
Innym popularnym 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 hermetyzacji stanu na poziomie aplikacji, takiego jak stan nawigacji i rozmiar ekranu. Pełny opis znajdziesz na stronie poświęconej logice interfejsu i zmiennej stanu.
Logika biznesowa
Jeśli funkcje kompozycyjne i proste klasy zmiennych stanu odpowiadają za logikę interfejsu i stan elementów interfejsu, zmienna stanu na poziomie ekranu odpowiada za te zadania:
- Zapewnianie dostępu do logiki biznesowej aplikacji, która zwykle znajduje się w innych warstwach hierarchii, takich jak warstwy biznesowe i danych.
- przygotowanie danych aplikacji do prezentacji na konkretnym ekranie, co staje się stanem interfejsu ekranu;
ViewModels jako właściciel stanu
Zalety modeli widoku AAC w programowaniu na Androida sprawiają, że nadają się one do zapewniania dostępu do logiki biznesowej i przygotowywania danych aplikacji do wyświetlenia na ekranie.
Gdy przenosisz stan interfejsu w ViewModel, przenosisz go poza kompozycję.
ViewModel jest przechowywany poza kompozycją.ViewModels nie są przechowywane jako część kompozycji. Są one dostarczane przez platformę i ograniczone do ViewModelStoreOwner, którym może być aktywność, fragment, wykres nawigacyjny lub miejsce docelowe wykresu nawigacyjnego. Więcej informacji o zakresach ViewModel znajdziesz w dokumentacji.
Wtedy ViewModel jest źródłem prawdy i najniższym wspólnym przodkiem stanu interfejsu.
Stan interfejsu ekranu
Zgodnie z powyższymi definicjami stan interfejsu ekranu jest generowany przez zastosowanie reguł biznesowych. Ponieważ odpowiada za to zmienna stanu na poziomie ekranu, oznacza to, że stan interfejsu ekranu jest zwykle przenoszony do zmiennej stanu na poziomie ekranu, w tym przypadku do ViewModel.
Weź pod uwagę ConversationViewModel aplikacji do obsługi czatu i sposób, w jaki ujawnia ona stan interfejsu ekranu i zdarzenia, które go modyfikują:
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) { /* ... */ } }
Komponenty korzystają ze stanu interfejsu ekranu przeniesionego do ViewModel. Aby zapewnić dostęp do logiki biznesowej, musisz wstrzyknąć instancję ViewModel do funkcji kompozycyjnych na poziomie ekranu.
Poniżej znajdziesz przykład elementu ViewModel używanego w funkcji kompozycyjnej na poziomie ekranu.
W tym przypadku funkcja kompozycyjna ConversationScreen() wykorzystuje stan interfejsu ekranu przeniesiony do 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) /* ... */ }
Przechodzenie do szczegółów właściwości
„Przekazywanie właściwości” odnosi się do przekazywania danych przez kilka zagnieżdżonych komponentów podrzędnych do lokalizacji, w której są odczytywane.
Typowym przykładem sytuacji, w której w Compose może wystąpić przekazywanie właściwości, jest wstrzyknięcie zmiennej stanu na poziomie ekranu na najwyższym poziomie i przekazanie stanu oraz zdarzeń do podrzędnych funkcji kompozycyjnych. Może to dodatkowo spowodować przeciążenie sygnatur funkcji typu „composable”.
Nawet jeśli udostępnianie zdarzeń jako pojedynczych 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 otoki do hermetyzacji stanu i zdarzeń w jednym miejscu, ponieważ zmniejsza widoczność funkcji kompozycyjnych. Dzięki temu, że nie masz klas otoki, jest też bardziej prawdopodobne, że będziesz przekazywać funkcjom kompozycyjnym tylko te parametry, których potrzebują, co jest sprawdzoną metodą.
Ta sama sprawdzona metoda ma zastosowanie, jeśli te zdarzenia są zdarzeniami nawigacji. Więcej informacji na ten temat znajdziesz w dokumentacji dotyczącej nawigacji.
Jeśli wykryjesz problem z wydajnością, możesz też odroczyć odczyt stanu. Więcej informacji znajdziesz w dokumentacji dotyczącej wydajności.
Stan elementu interfejsu
Możesz przenieść stan elementu interfejsu do zmiennej stanu na poziomie ekranu, jeśli istnieje logika biznesowa, która musi go odczytywać lub zapisywać.
Kontynuując przykład aplikacji do obsługi czatu, aplikacja wyświetla sugestie użytkowników na czacie grupowym, gdy użytkownik wpisze @ i podpowiedź. Te sugestie pochodzą z warstwy danych, a logika obliczania listy sugestii użytkownika jest uważana za logikę biznesową. Funkcja wygląda tak:
@ i podpowiedźPlik 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 wpisuje nowe dane, aplikacja wywołuje logikę biznesową, aby wygenerować suggestions.
suggestions to stan interfejsu ekranu, który jest wykorzystywany przez interfejs Compose przez zbieranie z StateFlow.
Caveat
W przypadku niektórych stanów elementów interfejsu Compose przenoszenie do ViewModel może wymagać szczególnej uwagi. Na przykład niektóre zmienne stanu elementów interfejsu Compose udostępniają metody modyfikowania stanu. Niektóre z nich mogą być funkcjami zawieszenia, które wywołują animacje. Te funkcje zawieszenia mogą zgłaszać wyjątki, jeśli wywołasz je z CoroutineScope, który nie jest ograniczony do kompozycji.
Załóżmy, że zawartość panelu aplikacji jest dynamiczna i po jego zamknięciu musisz ją pobrać i odświeżyć z warstwy danych. Stan panelu należy przenieść do ViewModel, aby można było wywołać zarówno interfejs, jak i logikę biznesową tego elementu z poziomu właściciela stanu.
Jednak wywołanie metody close() interfejsu DrawerState przy użyciu
viewModelScope z interfejsu Compose powoduje wyjątek czasu działania typu
IllegalStateException z komunikatem „
MonotonicFrameClock is not available in this
CoroutineContext”” ( nie jest dostępny w tym ).
Aby rozwiązać ten problem, użyj CoroutineScope o zakresie ograniczonym do kompozycji. Zapewnia MonotonicFrameClock w CoroutineContext, który jest niezbędny do działania funkcji zawieszenia.
Aby naprawić tę awarię, przełącz 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 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
- Architektura interfejsu Compose