Gdzie podnieść stan

W aplikacji Compose, w którym podnosisz stan interfejsu, zależy to od tego, czy niezbędna jest logika biznesowa lub logika biznesowa. W tym dokumencie opisujemy 2 główne w różnych sytuacjach.

Sprawdzona metoda

Należy przenieść stan interfejsu do najmniejszego wspólnego elementu nadrzędnego między wszystkimi elementów kompozycyjnych, które odczytują go i zapisują. Należy zachować stan najbliżej miejsca, w którym się znajduje i całego świata. Od właściciela stanu ujawniaj konsumentom stały stan i zdarzenia do zmiany stanu.

Najniższy wspólny element nadrzędny może też znajdować się poza kompozycją. Przykład: podczas przenoszenia stanu w ViewModel, ponieważ uwzględniana jest logika biznesowa.

Na tej stronie szczegółowo opisujemy tę sprawdzoną metodę i pamiętaj o pewnym zastrzeżeniu.

Typy stanu UI i logika interfejsu

Poniżej znajdziesz definicje typów logiki i stanów interfejsu użytkownika, które są używane w całym dokumencie.

Stan interfejsu

Stan interfejsu to właściwość opisująca interfejs użytkownika. Istnieją 2 typy UI województwo:

  • Stan UI ekranu określa, co ma się wyświetlać na ekranie. Na przykład plik NewsUiState zajęcia mogą zawierać artykuły z wiadomościami i inne potrzebne informacje do renderowania interfejsu użytkownika. Ten stan jest zwykle połączony z innymi warstwami ponieważ zawiera dane aplikacji.
  • Stan elementu interfejsu odnosi się do właściwości wchodzących w skład elementów interfejsu, które wpływa na sposób ich renderowania. Element interfejsu może być wyświetlany lub ukryty i może mają określoną czcionkę, jej rozmiar lub kolor. W widokach Androida sam zarządza tym stanem, ponieważ jest z natury stanową i naraża metody modyfikować jego stan ani wysyłać zapytania o jego stan. Na przykład get oraz set klasy TextView dla jej tekstu. W plecaku odrzutowym tworzenia, stan jest spoza funkcji kompozycyjnej i możesz ją nawet podnieść w pobliżu funkcji kompozycyjnej, do funkcji lub reprezentatora stanu. Przykład: ScaffoldState dla atrybutu Funkcja kompozycyjna Scaffold.

Logiczna

Logiką aplikacji może być logika biznesowa lub logika interfejsu użytkownika:

  • Logika biznesowa to implementacja wymagań dotyczących usług w przypadku aplikacji. i skalowalnych danych. Na przykład dodanie do zakładek artykułu w aplikacji czytnika wiadomości, gdy użytkownik klika przycisk. Działanie tej logiki zapisu zakładki w pliku lub bazie danych to umieszczone zwykle w domenach lub warstwach danych. Posiadacz stanu zazwyczaj przekazuje tę logikę do tych warstw, wywołując udostępniane przez nie metody.
  • Logiki interfejsu dotyczą sposobu wyświetlania stanu UI na ekranie. Dla: np. gdy użytkownik wybrał kategoria, przewijanie do konkretnego elementu na liście lub logika nawigacji na określony ekran, gdy użytkownik kliknie przycisk.

Logika UI

Gdy logika interfejsu musi odczytywać stan lub zapisywać, ustaw zakres tego stanu na interfejs użytkownika zgodnie z jego cyklem życia. Aby to osiągnąć, w funkcji kompozycyjnej należy przenieść stan na odpowiedni poziom. Ewentualnie możesz za pomocą prostej klasy posiadacza stanu, która jest też ograniczona do cyklu życia interfejsu użytkownika.

Poniżej znajduje się opis obu rozwiązań i wyjaśnienie, kiedy należy z nich korzystać.

Elementy kompozycyjne jako właściciel stanu

Umieszczenie logiki interfejsu i stanu elementu interfejsu w elementach kompozycyjnych to dobre podejście, jeśli i logikę selekcjonowania danych. Stan swojego stanu możesz pozostawić wewnętrznym możesz użyć podnośnika.

Nie potrzeba podnoszenia stanu

Stan podnoszenia nie zawsze jest wymagany. Stan może być przechowywany wewnętrznie w funkcji kompozycyjnej gdy żadna inna funkcja kompozycyjna nie musi jej kontrolować. 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 jest stanem wewnętrznym tego elementu interfejsu. Jest tylko odczytywane i modyfikowane w tej funkcji kompozycyjnej oraz stosowana do niej logika jest bardzo prosta. Podniesienie państwa w tym przypadku nie przyniesie więc dużych korzyści, ale może mieć charakter wewnętrzny. W ten sposób kompozytor stanie się właścicielem i singlem jako źródło danych o stanie rozwiniętym.

Unoszące się w elementach kompozycyjnych

Jeśli musisz udostępnić stan elementu interfejsu innym komponentom kompozycyjnym i interfejsem stosowania w różnych miejscach, możesz umieścić ją wyżej w hierarchii UI. Dzięki temu elementy kompozycyjne lepiej nadaje się do wielokrotnego użytku i można je łatwiej testować.

Poniższy przykład przedstawia aplikację do obsługi czatu, która ma 2 funkcje:

  • Przycisk JumpToBottom powoduje przewijanie listy wiadomości w dół. Przycisk wykonuje logikę interfejsu na liście.
  • Lista MessagesList przewija się w dół, gdy użytkownik wyśle nowe wiadomości. UserInput wykonuje logikę interfejsu użytkownika na podstawie stanu listy.
.
Aplikacja do obsługi czatu z przyciskiem JumpToDown i przewiń w dół w przypadku nowych wiadomości
Rysunek 1. Aplikacja do obsługi czatu z przyciskiem JumpToBottom i przewijaniem w dół w przypadku nowych wiadomości

Hierarchia kompozycyjna wygląda tak:

Drzewo kompozycji w Google Chat
Rysunek 2. Drzewo kompozycji w Google Chat
.

Stan LazyColumn jest przenoszony do ekranu rozmowy, dzięki czemu aplikacja może wykonywać logikę UI i odczytywać stan ze wszystkich elementów kompozycyjnych, które go wymagają:

Przeciąganie stanu leniwej kolumny z leniwej kolumny do ekranu rozmowy
Rysunek 3. Przenoszę stan LazyColumn z LazyColumn do ConversationScreen

Ostatecznie pliki kompozycyjne to:

Drzewo kompozycyjne Google Chat z opcją LazyListState podniesioną do ekranu ConversationScreen
Rysunek 4. Drzewo kompozycyjne czatu LazyListState podniesione do ConversationScreen

Kod wygląda następująco:

@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
        }
    })
}

Element LazyListState jest podniesiony do poziomu wymaganego przez logikę UI, która musi być zastosowano. Jest ona zainicjowana w funkcji kompozycyjnej, więc jest przechowywana w Kompozycja według cyklu życia.

Zwróć uwagę, że lazyListState jest zdefiniowany w metodzie MessagesList, przy czym klucz wartość domyślna to rememberLazyListState(). Jest to typowy wzorzec w funkcji tworzenia wiadomości. Dzięki temu elementy kompozycyjne stają się bardziej elastyczne i dają się wielokrotnego użytku. Możesz wtedy użyć funkcji kompozycyjnej, w różnych częściach aplikacji, które nie muszą kontrolować stanu. To jest co zwykle robi się podczas testowania lub wyświetlania podglądu elementu kompozycyjnego. Tak właśnie trzeba zrobić LazyColumn określa swój stan.

Najniższym wspólnym elementem nadrzędnym funkcji LazyListState jest ConversationScreen
Rysunek 5. Najniższy wspólny element nadrzędny funkcji LazyListState to ConversationScreen

Zwykła klasa posiadacza stanu jako właściciel stanu

Gdy funkcja kompozycyjna zawiera złożoną logikę UI obejmującą jeden lub wiele stanów elementu interfejsu, powinien delegować ten obowiązek określania stanu , np. klasy zwykłej właściciela stanu. Dzięki temu logika funkcji kompozycyjnej łatwiejsze testowanie w oderwaniu od siebie i zmniejsza jego złożoność. Takie podejście faworyzuje zasada rozdziału potencjalnych problemów: za rząd odpowiada elementom kompozycyjnym emitujących elementy UI, a element stanu zawiera logikę UI .

Zwykłe klasy posiadaczy stanu zapewniają wygodne funkcje rozmówcom funkcję kompozycyjną, więc nie muszą sami pisać tej logiki.

Te zwykłe klasy są tworzone i zapamiętywane w kompozycji. Ponieważ są zgodne z cyklem życia elementu kompozycyjnego, mogą przyjmować typy podane przez Utwórz bibliotekę, np. rememberNavController() lub rememberLazyListState().

Na przykład: LazyListState klasa została zaimplementowana w Compose, aby 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 instancji LazyColumn, w której przechowywane są scrollPosition dla tego elementu interfejsu. Ujawnia również metody modyfikowania pozycję przewijania (na przykład przewijanie do danego elementu).

Jak widać, zwiększenie zakresu odpowiedzialności elementu kompozycyjnego zwiększa niezbędne w przypadku państwa. Obowiązki mogą dotyczyć logiki interfejsu użytkownika ilość stanu, który należy śledzić.

Innym często spotykanym wzorcem jest użycie zwykłej klasy posiadacza stanu do obsługi złożoności funkcji kompozycyjnych głównych w aplikacji. Możesz użyć takiej klasy do uwzględnij stan na poziomie aplikacji, np. stan nawigacji i rozmiar ekranu. Kompletny tego opisu znajdziesz na stronie z elementami logicznymi interfejsu użytkownika i na stronie z opisem jego stanu.

Logika biznesowa

jeśli za logiką interfejsu użytkownika odpowiadają klasyom kompozycyjnym i klasy zwykłych właścicieli stanu, stan elementu interfejsu, właściciel stanu na poziomie ekranu odpowiada za: zadania:

  • Zapewnienie dostępu do logiki biznesowej aplikacji, która które są zwykle umieszczane w innych warstwach hierarchii, takich jak firmy, warstw danych.
  • Przygotowanie danych aplikacji do prezentacji na konkretnym ekranie, który staje się stanem UI ekranu.

ViewModels jako właściciel stanu

Zalety modeli AAC ViewModels w programowaniu na Androida sprawiają, że są one odpowiednie za zapewnienie dostępu do logiki biznesowej i przygotowanie danych aplikacji w celu prezentacji na ekranie.

Gdy podnosisz stan interfejsu w elemencie ViewModel, przenosisz go poza Kompozycja.

Stan przeniesiony do modelu widoku danych jest przechowywany poza kompozycją.
Rysunek 6. Stan przeniesiony do ViewModel jest przechowywany poza kompozycją.

Modele ViewModel nie są przechowywane w ramach kompozycji. Są one dostarczane przez i mają zakres ograniczony do ViewModelStoreOwner, który może być Aktywność, Fragment, wykres nawigacyjny lub miejsce docelowe wykresu nawigacyjnego. Dla: więcej informacji o zakresach ViewModel znajdziesz w dokumentacji.

ViewModel jest źródłem prawdy i najniższym wspólnym elementem nadrzędnym dla Stan interfejsu.

Stan interfejsu ekranu

Zgodnie z powyższymi definicjami stan interfejsu ekranu jest generowany przez zastosowanie wartości biznesowej. reguł. Biorąc pod uwagę, że za to odpowiada właściciel stanu na poziomie ekranu, oznacza, że stan interfejsu ekranu jest zwykle podnoszony na poziomie ekranu właściciela, w tym przypadku ViewModel.

Weź pod uwagę ConversationViewModel aplikacji do obsługi czatu i sposób, w jaki prezentuje ona ekran Stan interfejsu i zdarzenia do zmodyfikowania:

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 zapisany w ViewModel. Zalecenia wstrzyknij instancję ViewModel w elementach kompozycyjnych na poziomie ekranu, aby udostępnić dostęp do logiki biznesowej.

Poniżej znajdziesz przykład elementu ViewModel używanego w funkcji kompozycyjnej na poziomie ekranu. W tym przypadku funkcja ConversationScreen() kompozycyjna przetwarza dane o stanie UI ekranu 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 w budynkach

„Analiza usług” odnosi się do przekazywania danych przez kilka zagnieżdżonych elementów podrzędnych do miejsca, w którym są odczytywane.

Typowym przykładem możliwości odgłębiania właściwości w narzędziu Compose jest wstrzykiwanie reguły stanu na poziomie ekranu na najwyższym poziomie i przekazywania na urządzeniach kompozycyjnych dla dzieci. Może to też spowodować przeciążenie podpisy funkcji kompozycyjnych.

Mimo że ujawnienie zdarzeń jako poszczególnych parametrów lambda mogłoby przeciążyć podpisu funkcji, maksymalizuje to widoczność funkcji kompozycyjnej są obowiązki. Wystarczy rzut oka, by sprawdzić, do czego służy.

wędliny w usłudze są lepsze od tworzenia klas opakowań do hermetyzacji; stanu i zdarzeń w jednym miejscu, ponieważ zmniejsza to widoczność obowiązków kompozycyjnych. Poza tym nie masz klas kodu, przekazuje elementom kompozycyjnym tylko potrzebne parametry, co jest najlepszym ćwiczenie.

Ta sama sprawdzona metoda dotyczy zdarzeń związanych z nawigacją, dowiesz się więcej na ten temat z dokumentów nawigacyjnych.

Jeśli wykryjesz problem z wydajnością, możesz też odroczyć odczyt państwa. Więcej informacji znajdziesz w dokumentacji dotyczącej skuteczności.

Stan elementu interfejsu

Możesz przenieść stan elementu interfejsu do posiadacza stanu na poziomie ekranu, jeśli występuje to logika biznesowa, która musi ją odczytać lub zapisać.

Podobnie jak w przypadku aplikacji do obsługi czatu, aplikacja wyświetla sugestie użytkownika w na czacie grupowym, gdy użytkownik wpisuje @ i podpowiedź. Sugestie te pochodzą z warstwa danych i logika obliczania listy sugestii użytkowników są brane pod uwagę to logika biznesowa. Funkcja wygląda tak:

Funkcja, która wyświetla sugestie użytkowników na czacie grupowym, gdy użytkownik wpisze „@” i podpowiedź
Rysunek 7. Funkcja, która wyświetla sugestie użytkowników na czacie grupowym, gdy użytkownik wpisze @ i podpowiedź

Implementacja tej funkcji (ViewModel) będzie wyglądać 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 wejściowe, aplikacja wywołuje logikę biznesową, aby utworzyć suggestions.

suggestions to stan UI ekranu, który jest wykorzystywany w interfejsie tworzenia wiadomości przez od StateFlow.

Uwaga

W przypadku niektórych stanów elementu interfejsu tworzenia wiadomości przeniesienie do interfejsu ViewModel może wymagać ze szczególnymi względami. Na przykład niektóre posiadacze stanów elementów interfejsu Compose i udostępniania metod modyfikowania stanu. Niektóre z nich to funkcje zawieszania, uruchamiać animacje. Te funkcje zawieszania mogą zgłaszać wyjątki, jeśli wywołasz ich z CoroutineScope, które nie jest ograniczone do Kompozycja.

Załóżmy, że zawartość panelu aplikacji jest dynamiczna i musisz ją pobrać i odświeżyć. z warstwy danych po jej zamknięciu. Należy podnieść stan panelu, aby ViewModel, aby można było wywołać w tym elemencie zarówno logikę UI, jak i logikę biznesową od właściciela stanu.

Jednak wywołanie metody DrawerState close() za pomocą funkcji viewModelScope w interfejsie tworzenia wiadomości powoduje wyjątek typu środowiska wykonawczego IllegalStateException z komunikatem „a Opcja MonotonicFrameClock jest niedostępna w CoroutineContext”.

Aby rozwiązać ten problem, użyj właściwości CoroutineScope o zakresie ograniczonym do kompozycji. Zapewnia MonotonicFrameClock w CoroutineContext, która jest wymagana przez zawieszać funkcje tak, aby działały.

Aby naprawić tę awarię, przełącz CoroutineContext współprogramu w ViewModel do wartości, które są ograniczone 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 artykułach: z dodatkowymi zasobami.

Próbki

Ćwiczenia z programowania

Filmy

. .