Warstwa interfejsu

Zadaniem interfejsu jest wyświetlanie danych aplikacji na ekranie. Interfejs jest też głównym punktem interakcji z użytkownikiem. Za każdym razem, gdy dane się zmieniają, czy to z powodu interakcji użytkownika (np. naciśnięcia przycisku), czy też danych zewnętrznych (np. odpowiedzi sieci), interfejs użytkownika aktualizuje się, aby odzwierciedlić te zmiany. W praktyce interfejs jest wizualną reprezentacją stanu aplikacji pobranego z warstwy danych.

Jednak dane aplikacji, które otrzymujesz z warstwy danych, są zwykle w innym formacie niż informacje, które chcesz wyświetlić. Możesz na przykład potrzebować tylko części danych do interfejsu użytkownika lub musisz połączyć 2 różne źródła danych, aby wyświetlić informacje istotne dla użytkownika. Niezależnie od zastosowanej logiki musisz przekazać interfejsowi wszystkie informacje potrzebne do pełnego renderowania. Warstwa interfejsu to potok, który przekształca zmiany danych aplikacji w formę, którą interfejs może zaprezentować, a następnie wyświetla.

W typowej architekturze elementy interfejsu warstwy interfejsu zależą od zmiennych stanu, które z kolei zależą od klas z warstwy danych lub opcjonalnej warstwy domeny.
Rysunek 1. rola warstwy interfejsu w architekturze aplikacji;

Podstawowe studium przypadku

Weźmy na przykład aplikację, która pobiera artykuły, aby użytkownik mógł je przeczytać. Aplikacja ma ekran z artykułami, na którym wyświetlane są artykuły dostępne do przeczytania. Zalogowani użytkownicy mogą też dodawać do zakładek artykuły, które szczególnie im się podobają. Ponieważ w każdej chwili może być dostępnych wiele artykułów, czytelnik musi mieć możliwość przeglądania ich według kategorii. Podsumowując, aplikacja umożliwia użytkownikom:

  • Wyświetl artykuły dostępne do przeczytania.
  • Przeglądaj artykuły według kategorii.
  • Zaloguj się i dodaj do zakładek określone artykuły.
  • dostęp do niektórych funkcji premium (jeśli spełniasz wymagania).
Przykładowa aplikacja z wiadomościami, która wyświetla podglądy artykułów, z których jeden jest dodany do zakładek.
Rysunek 2. Przykładowa aplikacja z wiadomościami na potrzeby studium przypadku dotyczącego interfejsu.

W kolejnych sekcjach wykorzystamy ten przykład jako studium przypadku, aby przedstawić zasady jednokierunkowego przepływu danych i zilustrować problemy, które te zasady pomagają rozwiązać w kontekście architektury aplikacji w warstwie interfejsu.

Architektura warstwy interfejsu

Termin UI odnosi się do elementów interfejsu, takich jak kontenery i funkcje typu „composable”, które wyświetlają dane. Zalecanym zestawem narzędzi do tworzenia interfejsów na Androida jest Jetpack Compose. Rola warstwy danych polega na przechowywaniu danych aplikacji, zarządzaniu nimi i zapewnianiu do nich dostępu, dlatego warstwa interfejsu musi wykonać te czynności:

  1. wykorzystywać dane aplikacji i przekształcać je w dane, które interfejs może łatwo renderować;
  2. wykorzystywać dane możliwe do renderowania w interfejsie i przekształcać je w elementy interfejsu, które będą wyświetlane użytkownikowi;
  3. wykorzystywać zdarzenia wejściowe użytkownika z tych złożonych elementów interfejsu i w razie potrzeby odzwierciedlać ich efekty w danych interfejsu.
  4. Powtarzaj kroki od 1 do 3 tak długo, jak to konieczne.

W dalszej części tego przewodnika dowiesz się, jak wdrożyć warstwę interfejsu, która wykonuje te czynności. W tym przewodniku znajdziesz następujące tematy:

  • Jak zdefiniować stan interfejsu
  • Jednokierunkowy przepływ danych (UDF) jako sposób tworzenia stanu interfejsu i zarządzania nim
  • jak udostępniać stan interfejsu za pomocą danych dostępnych do obserwacji zgodnie z zasadami UDF;
  • Jak wdrożyć interfejs, który wykorzystuje obserwowalny stan interfejsu

Najważniejsza z nich to definicja stanu interfejsu.

Definiowanie stanu interfejsu

W opisanym wcześniej studium przypadku interfejs wyświetla listę artykułów wraz z metadanymi każdego z nich. Te informacje, które aplikacja prezentuje użytkownikowi, to stan interfejsu.

Innymi słowy, jeśli interfejs jest tym, co widzi użytkownik, stan interfejsu jest tym, co aplikacja mówi, że powinien zobaczyć. Interfejs i jego stan są jak 2 strony tej samej monety. Interfejs jest wizualną reprezentacją stanu. Wszelkie zmiany stanu interfejsu są natychmiast odzwierciedlane w interfejsie.

Interfejs jest wynikiem powiązania elementów interfejsu na ekranie ze stanem interfejsu.
Rysunek 3. Interfejs jest wynikiem powiązania elementów interfejsu na ekranie ze stanem interfejsu.

Rozważmy studium przypadku: aby spełnić wymagania aplikacji Wiadomości, informacje potrzebne do pełnego wyrenderowania interfejsu można umieścić w klasie danych NewsUiState zdefiniowanej w ten sposób:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    // ...
)

Więcej informacji o stanie interfejsu znajdziesz w artykule State and Jetpack Compose (Stan i Jetpack Compose).

Niezmienność

Definicja stanu interfejsu w poprzednim przykładzie jest niezmienna. Główną zaletą tego rozwiązania jest to, że niezmienne obiekty dają gwarancję dotyczącą stanu aplikacji w danym momencie. Dzięki temu interfejs może skupić się na swojej głównej roli: odczytywaniu stanu i odpowiednim aktualizowaniu elementów interfejsu. Nigdy nie modyfikuj stanu interfejsu bezpośrednio w interfejsie, chyba że sam interfejs jest jedynym źródłem danych. Naruszenie tej zasady powoduje, że ta sama informacja pochodzi z wielu źródeł, co prowadzi do niespójności danych i drobnych błędów.

Weźmy na przykład wcześniejsze studium przypadku. Jeśli flaga bookmarked w obiekcie NewsItemUiState ze stanu interfejsu zostanie zaktualizowana w klasie Activity, będzie ona konkurować z warstwą danych jako źródłem stanu dodania artykułu do zakładek. Niezmienne klasy danych są bardzo przydatne w zapobieganiu tego rodzaju niespójnościom.

Konwencje nazewnictwa w tym przewodniku

W tym przewodniku nazwy klas stanu interfejsu są nadawane na podstawie funkcji ekranu lub części ekranu, którą opisują. Konwencja jest następująca:

funkcjonalność + UiState.

Na przykład stan ekranu wyświetlającego wiadomości może mieć nazwę NewsUiState, a stan wiadomości na liście wiadomości może mieć nazwę NewsItemUiState.

Zarządzanie stanem za pomocą jednokierunkowego przepływu danych

W poprzedniej sekcji ustaliliśmy, że stan interfejsu to niezmienny zrzut szczegółów potrzebnych do renderowania interfejsu. Jednak dynamiczny charakter danych w aplikacjach oznacza, że stan może się zmieniać w czasie. Może to być spowodowane interakcją użytkownika lub innymi zdarzeniami, które modyfikują dane bazowe używane do wypełniania aplikacji.

W przypadku tych interakcji warto użyć mediatora, który będzie je przetwarzał, definiował logikę stosowaną do każdego zdarzenia i przekształcał źródła danych, aby utworzyć stan interfejsu. Chociaż te interakcje i ich logika mogą być przechowywane w samym interfejsie, może to szybko stać się nieporęczne, ponieważ interfejs przejmuje zbyt wiele odpowiedzialności. Może to też wpłynąć na możliwość testowania, ponieważ wynikowy kod jest ściśle powiązany. Jeśli stan interfejsu nie jest bardzo prosty, upewnij się, że jedynym zadaniem interfejsu jest wykorzystywanie i wyświetlanie stanu interfejsu.

W tej sekcji omówimy jednokierunkowy przepływ danych (UDF), czyli wzorzec architektury, który pomaga wymusić ten zdrowy podział odpowiedzialności.

Zmienne stanu

Zmienne stanu to klasy odpowiedzialne za generowanie stanu interfejsu i logikę potrzebną do wygenerowania tego stanu. Zmienne stanu mają różne rozmiary w zależności od zakresu odpowiednich elementów interfejsu, którymi zarządzają – od pojedynczego widżetu, takiego jak dolny pasek aplikacji, po cały ekran lub miejsce docelowe nawigacji.

W tym drugim przypadku typową implementacją jest instancja ViewModel, chociaż w zależności od wymagań aplikacji może wystarczyć prosta klasa. Na przykład aplikacja Wiadomości ze studium przypadku używa klasy NewsViewModel jako zmiennej stanu do generowania stanu interfejsu dla ekranu wyświetlanego w tej sekcji.

Istnieje wiele sposobów modelowania współzależności między interfejsem użytkownika a producentem stanu. Jednak ponieważ interakcję między interfejsem a jego klasą ViewModel można w dużej mierze rozumieć jako wejście zdarzenia i wynikające z niego wyjście stanu, relację można przedstawić w sposób pokazany na tym diagramie:

Dane aplikacji przepływają z warstwy danych do ViewModel. Stan interfejsu przechodzi z ViewModel do elementów interfejsu, a zdarzenia przechodzą z elementów interfejsu z powrotem do ViewModel.
Rysunek 4. Diagram przedstawiający działanie jednokierunkowego przepływu danych w architekturze aplikacji.

Wzorzec, w którym stan przepływa w dół, a zdarzenia w górę, nazywa się jednokierunkowym przepływem danych (UDF). Konsekwencje tego wzorca dla architektury aplikacji są następujące:

  • ViewModel przechowuje i udostępnia stan, który ma być używany przez interfejs. Stan interfejsu to dane aplikacji przekształcone przez ViewModel.
  • Interfejs powiadamia ViewModel o zdarzeniach użytkownika.
  • ViewModel obsługuje działania użytkownika i aktualizuje stan.
  • Zaktualizowany stan jest przekazywany z powrotem do interfejsu, aby go wyrenderować.
  • Powyższe czynności są powtarzane w przypadku każdego zdarzenia, które powoduje mutację stanu.

W przypadku miejsc docelowych lub ekranów nawigacji ViewModel współpracuje z repozytoriami lub klasami przypadków użycia, aby pobierać dane i przekształcać je w stan interfejsu, uwzględniając przy tym efekty zdarzeń, które mogą powodować zmiany stanu. Wspomniane wcześniej studium przypadku zawiera listę artykułów, z których każdy ma tytuł, opis, źródło, imię i nazwisko autora, datę publikacji oraz informację o tym, czy został dodany do zakładek. Interfejs każdego artykułu wygląda tak:

Pojedynczy artykuł z aplikacji z analizy przypadku. Interfejs zawiera miniaturę, tytuł artykułu, autora, szacowany czas czytania artykułu i ikonę zakładki.
Rysunek 5. Interfejs artykułu w aplikacji ze studium przypadku.

Przykładem zdarzenia, które może powodować zmiany stanu, jest prośba użytkownika o dodanie artykułu do zakładek. ViewModel jako producent stanu odpowiada za zdefiniowanie całej logiki wymaganej do wypełnienia wszystkich pól w stanie interfejsu i przetworzenia zdarzeń potrzebnych do pełnego wyrenderowania interfejsu.

Zdarzenie interfejsu występuje, gdy użytkownik dodaje artykuł do zakładek. ViewModel powiadamia warstwę danych o zmianie stanu. Warstwa danych zachowuje zmianę danych i aktualizuje dane aplikacji. Nowe dane aplikacji z artykułem dodanym do zakładek są przekazywane do ViewModel, który następnie tworzy nowy stan interfejsu i przekazuje go do elementów interfejsu w celu wyświetlenia.
Rysunek 6. Diagram przedstawiający cykl zdarzeń i danych w UDF.

W sekcjach poniżej przyjrzymy się bliżej zdarzeniom, które powodują zmiany stanu, oraz sposobom ich przetwarzania za pomocą funkcji UDF.

Typy logiki

Dodanie artykułu do zakładek jest przykładem logiki biznesowej, ponieważ zwiększa wartość aplikacji. Więcej informacji na ten temat znajdziesz na stronie poświęconej warstwie danych. Istnieją jednak różne typy logiki, które warto zdefiniować:

  • Logika biznesowa to implementacja wymagań dotyczących usług w odniesieniu do danych aplikacji. Jak już wspomnieliśmy, jednym z przykładów jest dodanie artykułu do zakładek w aplikacji ze studium przypadku. Logika biznesowa jest zwykle umieszczana w warstwach domeny lub danych, ale nigdy w warstwie interfejsu.
  • Logika zachowania interfejsu lub logika interfejsu określa, jak wyświetlać zmiany stanu na ekranie. Przykłady obejmują uzyskanie odpowiedniego tekstu do wyświetlenia na ekranie za pomocą Androida Resources, przejście do konkretnego ekranu, gdy użytkownik kliknie przycisk, lub wyświetlenie wiadomości użytkownika na ekranie za pomocą powiadomienia wyskakującego lub paska z powiadomieniem.

Logikę interfejsu zachowuj w interfejsie, a nie w komponencie ViewModel, zwłaszcza gdy dotyczy to typów interfejsu takich jak Context. Jeśli interfejs staje się coraz bardziej złożony i chcesz przekazać jego logikę innej klasie, aby ułatwić testowanie i rozdzielić zadania, możesz utworzyć prostą klasę jako zmienną stanu. Proste klasy utworzone w interfejsie mogą mieć zależności od pakietu Android SDK, ponieważ są zgodne z cyklem życia interfejsu. Obiekty ViewModel mają dłuższy cykl życia.

Więcej informacji o zmiennych stanu i o tym, jak pomagają w tworzeniu interfejsu, znajdziesz w przewodniku po stanie Jetpack Compose.

Dlaczego warto używać funkcji UDF?

UDF modeluje cykl produkcji stanu, jak pokazano na rysunku 4. Oddziela też miejsce, w którym powstają zmiany stanu, od miejsca, w którym są przekształcane, i miejsca, w którym są ostatecznie wykorzystywane. Dzięki temu interfejs użytkownika robi dokładnie to, co sugeruje jego nazwa: wyświetla informacje, obserwując zmiany stanu, i przekazuje intencje użytkownika, przekazując te zmiany do modelu ViewModel.

Innymi słowy, UDF umożliwia:

  • Spójność danych. Istnieje jedno źródło wiarygodnych danych dla interfejsu.
  • Możliwość testowania. Źródło stanu jest odizolowane, dzięki czemu można je testować niezależnie od interfejsu.
  • Łatwość konserwacji. Mutacja stanu przebiega według dobrze zdefiniowanego wzorca, w którym mutacje są wynikiem zarówno zdarzeń użytkownika, jak i źródeł danych, z których są pobierane.

Udostępnij stan interfejsu

Po zdefiniowaniu stanu interfejsu i określeniu, jak będziesz zarządzać jego tworzeniem, kolejnym krokiem jest zaprezentowanie utworzonego stanu w interfejsie.

Gdy używasz funkcji UDF do zarządzania generowaniem stanu, możesz uznać wygenerowany stan za strumień – innymi słowy, z czasem powstaje wiele wersji stanu. Udostępnij stan interfejsu w obserwowalnym obiekcie danych, takim jak StateFlow. Dzięki temu interfejs może reagować na wszelkie zmiany wprowadzone w stanie bez konieczności ręcznego pobierania danych bezpośrednio z ViewModel. Dzięki temu w pamięci podręcznej zawsze znajduje się najnowsza wersja stanu interfejsu, co jest przydatne do szybkiego przywracania stanu po zmianach konfiguracji.

class NewsViewModel(
    // ...
) : ViewModel() {

    val uiState: NewsUiState = /* ... */
}

Wprowadzenie do przepływów w Kotlinie znajdziesz w artykule Przepływy w Kotlinie na Androidzie. Aby dowiedzieć się, jak używać StateFlow jako obserwowalnego obiektu przechowującego dane, zapoznaj się z ćwiczeniem z programowania Stan zaawansowany i efekty uboczne w Jetpack Compose.

W przypadkach, gdy dane udostępniane interfejsowi są stosunkowo proste, często warto opakować je w typ stanu interfejsu, ponieważ przekazuje on relację między emisją zmiennej stanu a powiązanym z nią ekranem lub elementem interfejsu. W miarę jak element interfejsu staje się coraz bardziej złożony, można łatwo rozszerzyć definicję stanu interfejsu, aby uwzględnić dodatkowe informacje potrzebne do renderowania elementu interfejsu.

Typowym sposobem tworzenia strumienia UiState jest udostępnienie właściwości mutableStateOf za pomocą private set, dzięki czemu stan jest zmienny w ViewModel, ale tylko do odczytu dla interfejsu.

class NewsViewModel(
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    // ...
}

ViewModel może następnie udostępniać metody, które wewnętrznie zmieniają stan, publikując aktualizacje do wykorzystania przez interfejs. Załóżmy na przykład, że musisz wykonać działanie asynchroniczne. Możesz uruchomić współprogram za pomocą viewModelScope, a następnie po zakończeniu zaktualizować stan zmienny.

class NewsViewModel(
    private val repository: NewsRepository,
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

W poprzednim przykładzie klasa NewsViewModel próbuje pobrać artykuły z określonej kategorii, a następnie odzwierciedla wynik próby (powodzenie lub niepowodzenie) w stanie interfejsu, który może odpowiednio na niego zareagować. Więcej informacji o obsłudze błędów znajdziesz w sekcji Wyświetlanie błędów na ekranie.

Uwagi dodatkowe

Oprócz poprzednich wskazówek weź pod uwagę te kwestie podczas udostępniania stanu interfejsu:

  • Używaj pojedynczego obiektu stanu interfejsu do obsługi stanów, które są ze sobą powiązane. Dzięki temu kod jest bardziej spójny i łatwiejszy do zrozumienia. Jeśli udostępnisz listę wiadomości i liczbę zakładek w 2 różnych strumieniach, może się zdarzyć, że jeden z nich zostanie zaktualizowany, a drugi nie. Jeśli używasz jednego strumienia, oba elementy są aktualizowane. Ponadto niektóre elementy logiki biznesowej mogą wymagać połączenia źródeł. Możesz na przykład wyświetlać przycisk zakładki tylko wtedy, gdy użytkownik jest zalogowany oraz subskrybuje płatną usługę wiadomości. Klasę stanu interfejsu możesz zdefiniować w ten sposób:

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

    W tej deklaracji widoczność przycisku zakładki jest właściwością pochodną 2 innych właściwości. W miarę jak logika biznesowa staje się coraz bardziej złożona, posiadanie pojedynczej klasy UiState, w której wszystkie właściwości są natychmiast dostępne, staje się coraz ważniejsze.

  • Stany interfejsu: pojedynczy strumień czy wiele strumieni? Główną zasadą, którą należy się kierować przy wyborze między udostępnianiem stanu interfejsu w jednym strumieniu lub w wielu strumieniach, jest relacja między emitowanymi elementami. Największymi zaletami udostępniania pojedynczego strumienia są wygoda i spójność danych: konsumenci stanu zawsze mają dostęp do najnowszych informacji w danym momencie. Są jednak sytuacje, w których odpowiednie mogą być osobne strumienie stanu z ViewModel:

    • Niepowiązane typy danych: niektóre stany potrzebne do renderowania interfejsu mogą być całkowicie niezależne od siebie. W takich przypadkach koszty połączenia tych różnych stanów mogą przewyższać korzyści, zwłaszcza jeśli jeden z nich jest aktualizowany częściej niż drugi.

    • Porównywanie obiektów UiState: im więcej pól zawiera obiekt UiState, tym większe prawdopodobieństwo, że strumień zostanie wyemitowany w wyniku aktualizacji jednego z pól. Elementy interfejsu nie mają mechanizmu porównywania, który pozwalałby określić, czy kolejne emisje są różne czy takie same, dlatego każda emisja powoduje aktualizację elementu interfejsu. Oznacza to, że może być konieczne zastosowanie złagodzeń przy użyciu metod interfejsu API Flow, takich jak distinctUntilChanged().

Więcej informacji o renderowaniu i stanie interfejsu znajdziesz w artykule Cykl życia funkcji kompozycyjnych.

Wykorzystywanie stanu interfejsu

Aby wykorzystać strumień obiektów UiState w interfejsie, użyj operatora terminala dla typu danych dostępnych do obserwacji, którego używasz. Na przykład w przypadku przepływów Kotlin użyj metody collect() lub jej odmian.

Podczas korzystania z obiektów przechowujących dane dostępne do obserwacji w interfejsie użytkownika należy wziąć pod uwagę cykl życia interfejsu. Nie pozwól, aby interfejs obserwował stan interfejsu, gdy funkcja kompozycyjna nie jest wyświetlana użytkownikowi. Więcej informacji na ten temat znajdziesz w tym poście na blogu. Podczas korzystania z przepływów najlepiej jest obsługiwać kwestie związane z cyklem życia za pomocą odpowiedniego zakresu współprogramu i interfejsu API collectAsStateWithLifecycle:

@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)
    /* ... */
}

Pokaż operacje w toku

Prostym sposobem na reprezentowanie stanów wczytywania w klasie UiState jest pole logiczne:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    // ...
)

Wartość tej flagi wskazuje, czy w interfejsie jest wyświetlany pasek postępu.

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Wyświetlanie błędów na ekranie

Wyświetlanie błędów w interfejsie jest podobne do wyświetlania operacji w toku, ponieważ oba te stany można łatwo przedstawić za pomocą wartości logicznych, które oznaczają ich obecność lub brak. Błędy mogą jednak zawierać powiązany komunikat, który zostanie przekazany użytkownikowi, lub powiązane działanie, które ponawia nieudaną operację. Dlatego, gdy operacja w toku jest wczytywana lub nie, stany błędów mogą wymagać modelowania za pomocą klas danych, które zawierają metadane odpowiednie dla kontekstu błędu.

Rozważmy poprzedni przykład, w którym podczas pobierania artykułów wyświetlany był pasek postępu. Jeśli ta operacja zakończy się błędem, możesz wyświetlić użytkownikowi co najmniej 1 komunikat ze szczegółowymi informacjami o tym, co poszło nie tak.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    // ...
)

Możesz wtedy wyświetlić użytkownikowi komunikaty o błędach w postaci elementów interfejsu, takich jak paski powiadomień. Więcej informacji o tym, jak zdarzenia interfejsu są generowane i wykorzystywane, znajdziesz w artykule o zdarzeniach interfejsu.

Wątki i równoczesność

Upewnij się, że wszystkie zadania wykonywane w ViewModel są bezpieczne dla wątku głównego, czyli można je bezpiecznie wywoływać z wątku głównego. Warstwy danych i domeny odpowiadają za przenoszenie pracy do innego wątku.

Jeśli ViewModel wykonuje długo trwające operacje, odpowiada również za przeniesienie tej logiki do wątku w tle. Korutyny w języku Kotlin to świetny sposób na zarządzanie operacjami równoczesnymi, a komponenty architektury Jetpack zapewniają im wbudowaną obsługę. Więcej informacji o używaniu współprogramów w aplikacjach na Androida znajdziesz w artykule Współprogramy Kotlin na Androidzie.

Zmiany w nawigacji w aplikacji są często spowodowane emisjami podobnymi do zdarzeń. Na przykład po zalogowaniu się klasy SignInViewModel pole isSignedIn w obiekcie UiState może mieć wartość true. Wykorzystuj takie aktywatory tak samo jak te opisane we wcześniejszej sekcji Wykorzystywanie stanu interfejsu, ale odłóż implementację wykorzystania do komponentu nawigacji.

Więcej informacji o nawigacji w interfejsie znajdziesz w artykule Nawigacja 3.

Paging

Biblioteka stronicowania jest używana w interfejsie z typem o nazwie PagingData. PagingData reprezentuje i zawiera elementy, które mogą się zmieniać w czasie, czyli nie jest typem niezmiennym, dlatego nie należy go reprezentować w niezmiennym stanie interfejsu. Zamiast tego udostępnij go z ViewModel niezależnie w osobnym strumieniu.

Poniższy przykład pokazuje interfejs Compose API biblioteki Paging:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

Animacje

Aby zapewnić płynne przejścia w nawigacji najwyższego poziomu, przed rozpoczęciem animacji możesz poczekać, aż drugi ekran załaduje dane.

Więcej informacji o przejściach nawigacyjnych znajdziesz w artykułach Nawigacja 3 i Przejścia elementów wspólnych w Compose.

Dodatkowe materiały

Wyświetlanie treści

Przykłady

Te przykłady Google pokazują, jak korzystać z warstwy interfejsu. Zapoznaj się z nimi, aby zobaczyć, jak te wskazówki wyglądają w praktyce: