w aplikacji Compose, w której należy przenieść stan interfejsu, zależy od tego, czy wymaga tego logika UI czy logika biznesowa. Niniejszy dokument przedstawia te 2 główne scenariusze.
Sprawdzona metoda
Musisz przenosić stan interfejsu do najniższego wspólnego elementu nadrzędnego spośród wszystkich funkcji kompozycyjnych, które go odczytują i zapisują. Zadbaj o to, aby stan był najbliższy miejscu, w którym jest używany. Od właściciela stanu możesz udostępnić konsumentom stały stan i zdarzenia w celu jego modyfikacji.
Najniższy wspólny przodek może też znajdować się poza kompozycją. Dotyczy to na przykład sytuacji, w których trzeba podnosić stan w elemencie ViewModel
z powodu logiki biznesowej.
Na tej stronie znajdziesz szczegółowe omówienie tych sprawdzonych metod oraz zastrzeżenie, o których warto pamiętać.
Rodzaje stanu interfejsu i logiki UI
Poniżej znajdziesz definicje typów stanu i logiki UI używane w tym dokumencie.
Stan interfejsu
Stan interfejsu użytkownika to właściwość opisująca interfejs użytkownika. Są 2 rodzaje stanu interfejsu:
- Stan interfejsu ekranu to element, który ma być widoczny na ekranie. Klasa
NewsUiState
może na przykład zawierać artykuły z wiadomościami i inne informacje potrzebne do renderowania interfejsu użytkownika. Ten stan jest zwykle połączony z innymi warstwami hierarchii, ponieważ zawiera dane aplikacji. - Stan elementu interfejsu odnosi się do właściwości niepowiązanych z elementami interfejsu, które wpływają na sposób ich renderowania. Element interfejsu może być wyświetlany lub ukryty i może mieć
określoną czcionkę oraz jej rozmiar i kolor. W widokach danych Androida widok danych samodzielnie zarządza tym stanem, ponieważ jest on z założenia stanowy i udostępnia metody do modyfikowania stanu lub wysyłania do niego zapytań. Przykładami są metody
get
iset
klasyTextView
dotyczącej tekstu. W Jetpack Compose stan jest spoza elementu kompozycyjnego i można przenieść go nawet w sąsiedztwo obiektu kompozycyjnego do wywołującego funkcję kompozycyjnego lub do elementu stanu. Przykładem może byćScaffoldState
dla funkcji kompozycyjnejScaffold
.
Logiczna
Logika logiczna aplikacji może być zarówno logiką biznesową, jak i logiką UI:
- Logika biznesowa to implementacja wymagań dotyczących usługi w przypadku danych aplikacji. Przykładem może być dodanie artykułu do zakładek w aplikacji czytnika wiadomości, gdy użytkownik kliknie przycisk. Ta logika zapisywania zakładek do pliku lub bazy danych jest zwykle umieszczana w domenie lub warstwach danych. Właściciel stanu zazwyczaj przekazuje tę logikę do tych warstw, wywołując udostępniane przez nie metody.
- Logika interfejsu jest powiązana ze sposobem wyświetlania stanu interfejsu na ekranie. Może to być na przykład uzyskanie odpowiedniej podpowiedzi na pasku wyszukiwania, gdy użytkownik wybierze kategorię, przewinięcie listy do konkretnego elementu na liście lub logika nawigacji po kliknięciu przycisku przez użytkownika.
Logika interfejsu
Jeśli logika interfejsu użytkownika musi odczytywać lub zapisywać stan, ustaw zakres stanu na interfejs użytkownika zgodnie z jego cyklem życia. Aby to zrobić, w funkcji kompozycyjnej musisz przenieść stan na odpowiednim poziomie. Możesz też zrobić to w klasie posiadacza zwykłego stanu, której zakres jest także ograniczony do cyklu życia interfejsu użytkownika.
Poniżej znajdziesz opis obu rozwiązań i wyjaśnienie, których używać.
Elementy kompozycyjne jako właściciel stanu
Stosowanie logiki UI i stanu elementu interfejsu w komponentach to dobre podejście, jeśli stan i logika są proste. W razie potrzeby możesz pozostawić stan wewnętrzny w ramach funkcji kompozycyjnej lub podnośnika.
Nie trzeba przenosić stanu
Stan podniesienia nie zawsze jest wymagany. Stan można zachować wewnętrznie w komponencie, gdy żaden inny element kompozycyjny nie potrzebuje jego kontroli. Ten fragment kodu zawiera funkcję kompozycyjną, która rozwija się i zwija po dotknięciu:
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } // Apply simple UI logic ) if (showDetails) { Text(message.timestamp) } }
Zmienna showDetails
to stan wewnętrzny tego elementu interfejsu. Jest on odczytywany i modyfikowany tylko w tym elemencie kompozycyjnym, a stosowana do niego logika jest bardzo prosta.
W tym przypadku podnoszenie stanu nie przyniesie zbyt wielu korzyści, więc można zająć się tym procesem wewnętrznym. W ten sposób kompozycja ta staje się właścicielem
i jednym źródłem wiarygodnych danych stanu rozwiniętego.
Przenoszenie elementów kompozycyjnych
Jeśli chcesz udostępnić stan elementu interfejsu innym obiektom kompozycyjnym i zastosować do niego logikę UI w różnych miejscach, możesz przenieść go wyżej w hierarchii interfejsu. Dzięki temu obiekty kompozycyjne można łatwiej wykorzystywać i testować.
Oto przykład aplikacji do obsługi czatu, która ma 2 funkcje:
- Przycisk
JumpToBottom
przewija listę wiadomości na sam dół. Przycisk działa w interfejsie w stanie listy. - Gdy użytkownik wyśle nowe wiadomości, lista
MessagesList
przewija się na dół. UserInput wykonuje logikę interfejsu użytkownika w stanie listy.
Hierarchia kompozycyjna wygląda tak:
Stan LazyColumn
jest przenoszony do ekranu rozmowy, aby aplikacja mogła wykonywać logikę interfejsu użytkownika i odczytywać stan ze wszystkich elementów kompozycyjnych, które go wymagają:
Wreszcie funkcja kompozycyjna:
Kod 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 } }) }
Obiekt LazyListState
jest podnoszony tak wysoko, jak jest to wymagane przez logikę interfejsu, która musi zostać zastosowana. Ponieważ zainicjowano ją w funkcji kompozycyjnej, jest przechowywana w kompozycji zgodnie ze swoim cyklem życia.
Pamiętaj, że właściwość lazyListState
jest definiowana w metodzie MessagesList
, a wartość domyślna to rememberLazyListState()
. Jest to często spotykany wzorzec podczas tworzenia wiadomości.
Dzięki temu elementy kompozycyjne są bardziej elastyczne i wielokrotnego użytku. Następnie można używać funkcji kompozycyjnej w różnych częściach aplikacji,
które mogą nie wymagać kontrolowania stanu. Dzieje się tak zwykle podczas testowania funkcji kompozycyjnej lub wyświetlania jej podglądu. Tak określa ona stan LazyColumn
.
Klasa posiadacza zwykłego stanu jako właściciel stanu
Gdy funkcja kompozycyjna zawiera złożoną logikę interfejsu, która obejmuje jedno lub wiele pól stanu elementu interfejsu, powinien on przekazać tę odpowiedzialność podmiotom stanowym (np. klasa posiadacza zwykłego stanu). Dzięki temu logika kompozycji jest bardziej izolowana, co zmniejsza jej złożoność. To podejście preferuje zasadę rozdziału potencjalnych problemów: element kompozycyjny odpowiada za wysyłanie elementów interfejsu, a właściciel stanu zawiera logikę UI i stan elementu UI.
klasy posiadaczy plików ze zwykłym stanem udostępniają wygodne funkcje wywołujące funkcję kompozycyjną, dzięki czemu nie muszą oni samodzielnie pisać tej logiki.
Takie proste klasy są tworzone i zapamiętywane w kompozycji. Ponieważ są one zgodne z cyklem życia elementu kompozycyjnego, mogą przyjmować typy podane przez bibliotekę tworzenia, takie jak rememberNavController()
lub rememberLazyListState()
.
Przykładem może być klasa prostego operatora LazyListState
zaimplementowana w komponencie Tworzenie, by kontrolować złożoność UI 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 jest przechowywany scrollPosition
dla tego elementu interfejsu. Udostępnia też metody modyfikowania pozycji przewijania, np. przez przewijanie do wybranego elementu.
Jak widać, zwiększenie zakresu obowiązków w elemencie kompozycyjnym zwiększa potrzebę sprawowania władzy państwowej. Odpowiedzialność może dotyczyć logiki interfejsu lub samego stanu.
Innym typowym wzorcem jest używanie zwykłej klasy posiadacza stanu do obsługi złożoności funkcji kompozycyjnych w aplikacji głównej. Tej klasy można używać do herbaty na poziomie aplikacji, np. stanu nawigacji czy rozmiaru ekranu. Pełny opis tej funkcji znajdziesz na stronie funkcji interfejsu i jego stanu.
Logika biznesowa
Jeśli za logikę i stan elementów interfejsu użytkownika odpowiadają klasy obiektów kompozycyjnych i klasy prostego stanu, to za te działania odpowiada osoba odpowiedzialna za te zadania:
- Zapewnia dostęp do logiki biznesowej aplikacji umieszczonej zwykle w innych warstwach hierarchii, np. w warstwie biznesowej i danych.
- Przygotowanie danych aplikacji do prezentacji na określonym ekranie, który staje się stanem interfejsu ekranu.
ViewModels jako właściciel stanu,
Dzięki zaletom modeli AAC ViewModele podczas programowania na Androida są one odpowiednie do zapewniania dostępu do logiki biznesowej i przygotowywania danych aplikacji do prezentacji na ekranie.
Gdy przeciągniesz stan interfejsu do elementu ViewModel
, przeniesiesz go poza kompozycję.
Modele ViewModel nie są przechowywane w ramach kompozycji. Są one dostarczane przez platformę i mają zakres ograniczony do elementu ViewModelStoreOwner
, który może być aktywnością, fragmentem, wykresem nawigacyjnym lub miejscem docelowym wykresu nawigacyjnego. Więcej informacji o zakresach ViewModel
znajdziesz w dokumentacji.
Następnie ViewModel
jest źródłem wiarygodnych danych i najniższym wspólnym przodkiem stanu interfejsu.
Stan interfejsu ekranu
Zgodnie z powyższymi definicjami stan interfejsu ekranu jest określany przez zastosowanie reguł biznesowych. Biorąc pod uwagę, że za to odpowiedzialny jest właściciel stanu na poziomie ekranu, oznacza to, że stan interfejsu użytkownika jest zwykle przenoszony do rejestrującego stan na poziomie ekranu, w tym przypadku do elementu ViewModel
.
Przyjrzyjmy się elementowi ConversationViewModel
aplikacji do obsługi czatu oraz temu, w jaki sposób określa on stan interfejsu ekranu i zdarzenia w celu jego modyfikacji:
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) { /* ... */ } }
Elementy kompozycyjne wykorzystują stan interfejsu ekranu podany w elemencie ViewModel
. Zalecamy wstrzyknięcie instancji ViewModel
w kompozycje na poziomie ekranu, aby zapewnić dostęp do logiki biznesowej.
Poniżej znajdziesz przykład uprawnienia ViewModel
używanego w funkcji kompozycyjnej na poziomie ekranu.
W tym przypadku kompozycyjny ConversationScreen()
korzysta ze stanu interfejsu ekranu podanego 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) /* ... */ }
Odwierty nieruchomości
Pozyskiwanie szczegółowych informacji o usłudze oznacza przekazywanie danych przez kilka zagnieżdżonych komponentów podrzędnych do miejsca, w którym zostaną odczytane.
Typowym przykładem sytuacji, w której wyszukiwanie właściwości może pojawić się w sekcji Utwórz, jest wstrzyknięcie operatora stanu na poziomie ekranu na najwyższym poziomie i przekazanie stanu i zdarzeń do podrzędnych elementów kompozycyjnych. Może to dodatkowo spowodować przeciążenie podpisów funkcji kompozycyjnych.
Ujawnienie zdarzeń jako poszczególnych parametrów lambda może przeciążyć podpis funkcji, ale maksymalizuje widoczność zadań funkcji kompozycyjnej. Wystarczy rzut oka, żeby sprawdzić, do czego służy.
Prowadzenie szczegółowego widoku usługi jest lepsze niż tworzenie klas opakowań do przechowywania stanu i zdarzeń w jednym miejscu, ponieważ zmniejsza to widoczność kompozycyjnych zakresów. Jeśli nie będziesz mieć klas kodu, z większym prawdopodobieństwem będziesz przekazywać obiekty kompozycyjne tylko te parametry, których są im potrzebne. Jest to najlepsza metoda.
Ta sama sprawdzona metoda dotyczy zdarzeń związanych z nawigacją. Więcej informacji na ten temat znajdziesz w dokumentach nawigacyjnych.
Jeśli wykryjesz problem z wydajnością, możesz też opóźnić odczyt stanu. Więcej informacji znajdziesz w dokumentacji dotyczącej skuteczności.
Stan elementu interfejsu
Możesz przenieść stan elementu interfejsu do właściciela stanu na poziomie ekranu, jeśli istnieje logika biznesowa, która musi go odczytać lub zapisać.
Nawiązując do przykładu aplikacji do obsługi czatu – aplikacja wyświetla sugestie użytkowników na czacie grupowym, gdy wpisze on @
oraz podpowiedź. Sugestie te pochodzą z warstwy danych, a logika obliczania listy sugestii użytkowników jest uznawana za logikę biznesową. Funkcja wygląda tak:
Zasób (ViewModel
), który implementuje 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 wejściowe, aplikacja wywołuje logikę biznesową, aby wygenerować suggestions
.
suggestions
to stan interfejsu ekranu. Jest wykorzystywany z interfejsu tworzenia wiadomości przez zbieranie danych z StateFlow
.
Zastrzeżenie
W przypadku niektórych stanów elementów interfejsu tworzenia wiadomości przeniesienie do ViewModel
może wymagać specjalnych uwag. Na przykład niektórzy właściciele stanów elementów interfejsu tworzenia wiadomości ujawniają metody modyfikowania stanu. Mogą to być na przykład funkcje zawieszania,
które uruchamiają animacje. Te funkcje zawieszania mogą zgłaszać wyjątki, jeśli wywołasz je z obiektu CoroutineScope
, który nie jest ograniczony do kompozycji.
Załóżmy, że zawartość panelu aplikacji jest dynamiczna i trzeba ją pobrać i odświeżyć z warstwy danych po zamknięciu. Przenieś stan panelu do obiektu ViewModel
, aby móc wywoływać w tym elemencie zarówno interfejs użytkownika, jak i logikę biznesową od właściciela stanu.
Wywołanie metody close()
w usłudze DrawerState
za pomocą elementu viewModelScope
z poziomu interfejsu tworzenia powoduje jednak wystąpienie wyjątku środowiska wykonawczego typu IllegalStateException
z komunikatem „MonotonicFrameClock
”CoroutineContext”
Aby rozwiązać ten problem, użyj uprawnienia CoroutineScope
o zakresie na poziomie kompozycji. Udostępnia w elemencie CoroutineContext
element MonotonicFrameClock
, który jest niezbędny do działania funkcji zawieszania.
Aby naprawić tę awarię, przełącz CoroutineContext
współpracy w ViewModel
na taką, która jest przypisana 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.
Próbki
Ćwiczenia z programowania
Filmy
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Zapisywanie stanu interfejsu użytkownika w momencie tworzenia
- Listy i siatki
- Tworzenie interfejsu użytkownika tworzenia wiadomości