Warstwa interfejsu

Rola interfejsu użytkownika to wyświetlanie danych aplikacji na ekranie i stanowienie głównego punktu interakcji z użytkownikiem. Za każdym razem, gdy dane ulegają zmianie (np. w wyniku interakcji użytkownika (np. naciśnięcia przycisku)) lub z zewnątrz (np. odpowiedzi sieci), należy zaktualizować interfejs, aby uwzględniał te zmiany. Interfejs stanowi wizualną reprezentację stanu aplikacji pobranej z warstwy danych.

Jednak dane aplikacji uzyskiwane z warstwy danych mają zwykle inny format niż informacje, które musisz wyświetlić. Możesz na przykład potrzebować tylko części danych do interfejsu użytkownika lub scalić 2 różne źródła danych, aby przedstawić informacje istotne dla danego użytkownika. Niezależnie od stosowanej logiki musisz przekazać w interfejsie wszystkie informacje potrzebne do pełnego wyrenderowania. Warstwa interfejsu to potok, który konwertuje zmiany danych aplikacji do postaci, którą interfejs może zaprezentować, a potem go wyświetlić.

W typowej architekturze elementy interfejsu warstwy UI zależą od stanów, które z kolei zależą od klas z warstwy danych lub opcjonalnej warstwy domeny.
Rysunek 1. Rola warstwy UI w architekturze aplikacji.

Podstawowe studium przypadku

Weźmy aplikację, która pobiera artykuły z wiadomościami, aby użytkownik mógł je przeczytać. Aplikacja ma ekran z artykułami, na którym prezentowane są artykuły dostępne do przeczytania, a także umożliwia zalogowanym użytkownikom dodawanie do zakładek najważniejszych artykułów. Ponieważ w danej chwili dostępnych jest wiele artykułów, czytelnik powinien 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 wybrane artykuły do zakładek.
  • Korzystaj z funkcji premium, jeśli się kwalifikujesz.
Rysunek 2. Przykładowa aplikacja z wiadomościami jako studium przypadku UI.

W kolejnych sekcjach posłużymy się tym przykładem jako studium przypadku, w którym przedstawimy zasady jednokierunkowego przepływu danych oraz wskażemy problemy, które te zasady pomagają rozwiązywać w kontekście architektury aplikacji w warstwie interfejsu.

architektura warstwy UI

Termin UI odnosi się do elementów interfejsu, takich jak działania i fragmenty, które wyświetlają dane, niezależnie od interfejsów API używanych do tego celu (Views lub Jetpack Compose). Rola warstwy danych to przechowywanie danych aplikacji, zarządzanie nimi oraz zapewnianie do nich dostępu, dlatego warstwa interfejsu musi wykonać te czynności:

  1. Pobieranie danych aplikacji i przekształcanie ich w dane, które interfejs może łatwo renderować.
  2. Pobieranie danych do renderowania w interfejsie i przekształcanie ich w elementy interfejsu do prezentacji użytkownikowi.
  3. Wykorzystuj zdarzenia wejściowe użytkownika z tych połączonych elementów interfejsu i w razie potrzeby odzwierciedlają ich wpływ w danych UI.
  4. Powtarzaj kroki od 1 do 3 tak długo, jak to konieczne.

W pozostałej części tego przewodnika dowiesz się, jak wdrożyć warstwę interfejsu, która wykonuje te czynności. W szczególności omówiono następujące zadania i koncepcje:

  • Definiowanie stanu interfejsu użytkownika.
  • Jednokierunkowy przepływ danych (UDF) służący do tworzenia stanu interfejsu i zarządzania nim.
  • Jak udostępniać stan interfejsu z typami danych możliwymi do obserwowania zgodnie z zasadami UDF.
  • Jak wdrożyć interfejs użytkownika, który wykorzystuje dostrzegalny stan UI.

Najważniejsze z nich jest definicja stanu interfejsu użytkownika.

Zdefiniuj stan interfejsu użytkownika

Zapoznaj się z omówionym wcześniej studium przypadku. Krótko mówiąc, interfejs pokazuje listę artykułów wraz z metadanymi każdego artykułu. Informacje, które aplikacja przekazuje użytkownikowi, to stan interfejsu użytkownika.

Inaczej mówiąc: jeśli użytkownik widzi interfejs, stan UI powinien wskazywać, że jest on widoczny w aplikacji. UI to wizualna reprezentacja jego stanu, podobnie jak 2 strony tej samej monety. Wszelkie zmiany stanu interfejsu są natychmiast odzwierciedlane w interfejsie.

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

Zapoznaj się ze studium przypadku. Aby spełnić wymagania aplikacji Wiadomości, informacje wymagane do pełnego wyrenderowania UI mogą być zawarte 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,
    ...
)

Niezmienność

W powyższym przykładzie definicji stanu interfejsu użytkownika nie można zmienić. Główną zaletą takiego rozwiązania jest to, że obiekty stałe zapewniają gwarancję stanu aplikacji w odpowiednim momencie. Dzięki temu interfejs może się skupić na jednej roli, czyli odczytywania stanu i odpowiednim aktualizowaniu elementów interfejsu. W związku z tym nie należy zmieniać stanu interfejsu bezpośrednio w nim, chyba że sam interfejs jest jedynym źródłem danych. Naruszenie tej zasady powoduje powstawanie wielu źródeł prawdy dotyczących tego samego fragmentu informacji, co prowadzi do niespójności danych i subtelnych błędów.

Jeśli na przykład flaga bookmarked w obiekcie NewsItemUiState stanu interfejsu użytkownika w studium przypadku zostanie zaktualizowana w klasie Activity, ta flaga konkuruje z warstwą danych jako źródłem informacji o stanie artykułu dodanym do zakładek. Stałe klasy danych są bardzo przydatne w zapobieganiu tego typu zjawisk antyplagiatowych.

Konwencje nazewnictwa w tym przewodniku

W tym przewodniku nazwy klas stanu interfejsu zależą od funkcjonalności danego ekranu lub jego części. Konwencja jest następująca:

functionality + UiState.

Na przykład stan ekranu z wiadomościami może mieć nazwę NewsUiState, a stan elementu wiadomości na liście wiadomości – NewsItemUiState.

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

W poprzedniej sekcji twierdziliśmy, że stan interfejsu to stały zrzut szczegółów wymaganych do wyrenderowania interfejsu. Dynamiczny charakter danych w aplikacjach oznacza jednak, że ich stan może się z czasem zmieniać. 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 może skorzystać z możliwości mediacji w ich przetwarzaniu, określenia logiki stosowanej do każdego zdarzenia i przeprowadzenia niezbędnych przekształceń do bazowych źródeł danych w celu utworzenia stanu interfejsu użytkownika. Te interakcje i ich logika można zawrzeć w samym interfejsie użytkownika, ale szybko staje się to nieporęczne, ponieważ interfejs staje się bardziej nieporęczny, niż sugeruje jego nazwa: staje się właścicielem, producentem, transformerem i nie tylko. Co więcej, może to wpływać na możliwość testowania, ponieważ otrzymany kod jest ściśle sprzężonym amalgamem bez widocznych granic. Dzięki temu interfejs użytkownika może korzystać ze zmniejszonego obciążenia. O ile stan UI nie jest bardzo prosty, jedynym obowiązkiem jego użytkownika jest spożywanie i wyświetlanie jego stanu.

W tej sekcji omawiamy jednokierunkowy przepływ danych (UDF) – wzorzec architektury, który pomaga wyegzekwować odpowiednie rozdzielenie odpowiedzialności.

Właściciele stanów

Klasy, które odpowiadają za tworzenie stanu interfejsu i zawierają logikę niezbędną do wykonania tego zadania, są nazywane właścicielami stanów. Elementy interfejsu mają różne rozmiary w zależności od zakresu elementów interfejsu, którymi zarządzają: od pojedynczego widżetu, np. dolnego paska aplikacji, po cały ekran lub miejsce docelowe nawigacji.

W drugim przypadku typowa implementacja to wystąpienie ViewModel, chociaż zależnie od wymagań aplikacji może wystarczyć klasa prosta. Na przykład aplikacja Wiadomości pochodząca z studium przypadku używa klasy NewsViewModel jako wartości stanu, aby utworzyć stan interfejsu ekranu wyświetlanego w tej sekcji.

Istnieje wiele sposobów modelowania zależności między interfejsem użytkownika a producentem stanu. Interakcję między interfejsem użytkownika a jego klasą ViewModel można w dużej mierze zinterpretować jako parametr wejściowy zdarzenia i jego stan output, więc można go przedstawić w taki sposób:

Dane aplikacji przepływają z warstwy danych do modelu ViewModel. Stan interfejsu użytkownika przechodzi z obiektu ViewModel do elementów interfejsu, a zdarzenia są przekazywane z elementów interfejsu z powrotem do obiektu ViewModel.
Rysunek 4. Schemat działania funkcji UDF w architekturze aplikacji.

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

  • ViewModel blokuje i ujawnia stan do wykorzystania przez interfejs użytkownika. Stan interfejsu użytkownika to dane aplikacji przekształcone przez model ViewModel.
  • Interfejs użytkownika powiadamia obiekt ViewModel o zdarzeniach użytkownika.
  • Model ViewModel obsługuje działania użytkowników i aktualizuje stan.
  • Zaktualizowany stan jest przesyłany z powrotem do interfejsu użytkownika w celu wyrenderowania.
  • Powyższe czynności powtarza się w przypadku każdego zdarzenia, które powoduje mutację stanu.

W przypadku miejsc docelowych i ekranów nawigacji obiekt ViewModel współpracuje z repozytoriami i klasami przypadków użycia, aby pobierać dane i przekształcać je w stan interfejsu użytkownika z uwzględnieniem efektów zdarzeń, które mogą powodować mutacje stanu. Wcześniej wspomniane 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ę, czy został on dodany do zakładek. Interfejs każdego elementu artykułu wygląda tak:

Rysunek 5. Interfejs przedstawiający element artykułu w aplikacji ze studium przypadku.

Przykładem zdarzenia, które może powodować mutacje stanu, jest użytkownik, który chce dodać artykuł do zakładek. Jako producent stanu odpowiada modelowi ViewModel, który odpowiada za zdefiniowanie wszystkich działań logicznych wymaganych do wypełnienia wszystkich pól w stanie UI i przetwarzania zdarzeń niezbędnych do pełnego wyrenderowania interfejsu.

Zdarzenie interfejsu ma miejsce, gdy użytkownik doda artykuł do zakładek. ViewModel powiadamia warstwę danych o zmianie stanu. Warstwa danych utrzymuje się na zmianie danych i aktualizuje dane aplikacji. Nowe dane aplikacji z artykułem dodanym do zakładek są przekazywane do modelu ViewModel, który następnie tworzy nowy stan interfejsu użytkownika 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 omawiamy zdarzenia, które powodują zmiany stanu, oraz sposób ich przetwarzania za pomocą funkcji UDF.

Rodzaje logiki

Dodanie artykułu do zakładek jest przykładem logiki biznesowej, ponieważ nadaje jej wartość dla aplikacji. Więcej informacji znajdziesz na stronie dotyczącej warstwy danych. Istnieją jednak różne rodzaje logiki, które trzeba zdefiniować:

  • Logika biznesowa to implementacja wymagań dotyczących usługi w przypadku danych aplikacji. Jak już wspomnieliśmy, jednym z przykładów jest dodawanie do zakładek artykułu w aplikacji ze studium przypadku. Logika biznesowa jest zwykle umieszczana w domenie lub warstwach danych, ale nigdy w warstwie interfejsu.
  • Logika działania interfejsu lub logika UI to sposób wyświetlania zmian stanu na ekranie. Może to być na przykład wyświetlenie odpowiedniego tekstu na ekranie za pomocą Androida Resources, przejście na konkretny ekran, gdy użytkownik kliknie przycisk, lub wyświetlenie komunikatu dla użytkownika za pomocą komunikatora lub paska powiadomień.

Logika UI, zwłaszcza gdy korzysta z typów interfejsów takich jak Context, powinna znajdować się w interfejsie, a nie w modelu ViewModel. Jeśli interfejs użytkownika staje się coraz bardziej złożony i chcesz przekazać logikę interfejsu do innej klasy, aby preferować możliwość testowania i oddzielenie problemów, możesz utworzyć klasę prostą prostą. Proste klasy utworzone w interfejsie mogą przyjąć zależności od pakietu Android SDK, ponieważ są zgodne z cyklem życia interfejsu użytkownika. Obiekty ViewModel mają dłuższy okres ważności.

Więcej informacji na temat właścicieli stanów i ich wpływu na kontekst pomagania w obsłudze interfejsu kompilacji znajdziesz w przewodniku po stanie łączenia usługi Jetpack Compose (w języku angielskim).

Dlaczego warto korzystać z funkcji UDF?

UDF modeluje cykl produkcji stanu, tak jak na Rysunku 4. Oddziela także miejsce, z którego pochodzą zmiany stanu, miejsce ich przekształcania i miejsce ich końcowego użycia. Dzięki temu interfejs może działać dokładnie tak, jak wskazuje jej nazwa: wyświetlać informacje, obserwując zmiany stanu, i przekazywać intencje użytkownika, przekazując te zmiany do modelu ViewModel.

Inaczej mówiąc, funkcja UDF umożliwia:

  • Spójność danych. Dla interfejsu użytkownika istnieje jedno źródło wiarygodnych informacji.
  • Testowanie. Źródło stanu jest izolowane i dlatego można testować niezależnie od interfejsu użytkownika.
  • Konserwacja. Mutacja stanu przebiega zgodnie z wyraźnie zdefiniowanym wzorcem, w którym mutacje są wynikiem zarówno zdarzeń użytkownika, jak i źródeł danych, z których pochodzą.

Udostępnianie stanu interfejsu

Gdy zdefiniujesz stan interfejsu użytkownika i określisz, w jaki sposób będziesz zarządzać jego tworzeniem, w następnym kroku możesz zobaczyć stan interfejsu użytkownika. Ponieważ używasz funkcji UDF do zarządzania produkcją stanu, można traktować wyprodukowany stan jako strumień – to znaczy, że w miarę upływu czasu powstanie wiele jego wersji. Dlatego należy ujawniać stan interfejsu w obserwowalnym elemencie danych, takim jak LiveData lub StateFlow. Dzieje się tak, ponieważ interfejs użytkownika może reagować na wszelkie zmiany stanu bez konieczności ręcznego pobierania danych bezpośrednio z modelu ViewModel. Zaletą tych typów jest również to, że zawsze najnowsza wersja stanu interfejsu użytkownika jest przechowywana w pamięci podręcznej, co jest przydatne przy szybkim przywracaniu stanu po zmianach w konfiguracji.

Wyświetlenia

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

    val uiState: StateFlow<NewsUiState> = …
}

Utwórz

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

    val uiState: NewsUiState = …
}

Wprowadzenie do obserwowalnego właściciela danych LiveData znajdziesz w tym ćwiczeniu w programowaniu. Podobne wprowadzenie do procesów Kotlin znajdziesz w tym artykule.

Jeśli dane ujawnione w interfejsie są stosunkowo proste, często warto je przedstawić w postaci stanu interfejsu, ponieważ przekazuje on relację między emisją użytkownika stanu a powiązanym z nim ekranem lub elementem interfejsu. Co więcej, w miarę jak element interfejsu staje się bardziej złożony, zawsze łatwiej jest dodać do definicji stanu UI, aby uwzględnić dodatkowe informacje potrzebne do renderowania tego elementu.

Typowym sposobem tworzenia strumienia danych UiState jest udostępnianie wstecznego strumienia zmiennego strumienia jako stałego strumienia z modelu ViewModel, np. ujawnienie parametru MutableStateFlow<UiState> jako właściwości StateFlow<UiState>.

Wyświetlenia

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Utwórz

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

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

Obiekt ViewModel może następnie ujawniać metody, które wewnętrznie zmieniają stan, publikując aktualizacje interfejsu użytkownika. Przeanalizujmy przypadek, w którym trzeba wykonać działanie asynchroniczne, uruchomić współprogram za pomocą narzędzia viewModelScope, a po zakończeniu zaktualizować stan zmienny.

Wyświetlenia

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

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

Utwórz

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 powyższym przykładzie klasa NewsViewModel próbuje pobrać artykuły z określonej kategorii, a potem odzwierciedla wynik próby (sukces lub niepowodzenie) w stanie interfejsu, w którym interfejs może odpowiednio zareagować. Więcej informacji o obsłudze błędów znajdziesz w sekcji Wyświetlanie błędów na ekranie.

Dodatkowe uwagi

Oprócz korzystania z poprzednich wskazówek podczas ujawniania stanu interfejsu musisz też wziąć pod uwagę te kwestie:

  • Obiekt stanu interfejsu powinien obsługiwać stany, które są ze sobą powiązane. Zmniejsza to liczbę niespójności i ułatwia zrozumienie kodu. Jeśli ujawnisz listę wiadomości i liczbę zakładek w dwóch różnych strumieniach, może dojść do sytuacji, w której jeden z nich został zaktualizowany, a w drugim – nie. Gdy korzystasz z jednego strumienia, obydwa elementy są zawsze aktualne. Poza tym niektóre logiki biznesowe mogą wymagać połączenia źródeł. Na przykład przycisk zakładki może być wyświetlany tylko wtedy, gdy użytkownik jest zalogowany oraz subskrybuje usługę wiadomości premium. Klasa stanu interfejsu użytkownika można zdefiniować w następujący 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ą derywowaną 2 innych usług. W miarę jak logika biznesowa staje się coraz bardziej złożona, coraz ważniejsze staje się stosowanie pojedynczej klasy UiState, w której wszystkie usługi są od razu dostępne.

  • Stany interfejsu: pojedynczy strumień czy wiele strumieni? Najważniejszą zasadą przy wyborze między ujawnianiem stanu interfejsu w pojedynczym strumieniu a w wielu strumieniach jest relacja między emitowanymi elementami. Największą zaletą ekspozycji z pojedynczego strumienia są wygoda i spójność danych: konsumenci w stanach zawsze mają dostęp do najnowszych informacji w dowolnym momencie. W niektórych sytuacjach odpowiednie mogą być jednak osobne strumienie stanu z modelu ViewModel:

    • Niepowiązane typy danych: niektóre stany wymagane do renderowania interfejsu mogą być od siebie całkowicie niezależne. W takich sytuacjach koszty związane z połączeniem odmiennych stanów mogą przeważać nad korzyściami, zwłaszcza jeśli jeden z nich jest aktualizowany częściej niż drugi.

    • Różnice UiState: im więcej pól znajduje się w obiekcie UiState, tym większe prawdopodobieństwo, że strumień zostanie wyemitowany w wyniku aktualizacji jednego z jego pól. Widoki nie mają własnego mechanizmu pozwalającego określić, czy kolejne emisje są inne czy takie same, dlatego każda emisja powoduje aktualizację widoku. Oznacza to, że konieczne może być zastosowanie złagodzeń za pomocą interfejsów API Flow lub metod takich jak distinctUntilChanged() w LiveData.

Wykorzystaj stan interfejsu użytkownika

Aby wykorzystywać w interfejsie strumień obiektów UiState, musisz używać operatora terminala dla używanego typu danych dostępnych do obserwacji. Na przykład w przypadku LiveData używasz metody observe(), a w przepływach Kotlin – metody collect() lub jej odmian.

Korzystając z obserwowalnych właścicieli danych w interfejsie, pamiętaj o uwzględnieniu cyklu życia interfejsu użytkownika. To ważne, ponieważ interfejs nie powinien obserwować stanu, gdy dany widok nie jest wyświetlany użytkownikowi. Więcej informacji na ten temat znajdziesz w tym poście na blogu. Gdy używasz LiveData, LifecycleOwner pośrednio radzi sobie z problemami dotyczącymi cyklu życia. Jeśli korzystasz z przepływów, najlepiej jest to robić z odpowiednim zakresem koordynacji i interfejsem repeatOnLifecycle API:

Wyświetlenia

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Utwórz

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Pokaż operacje w toku

Prostym sposobem na przedstawienie stanów wczytywania w klasie UiState jest użycie pola wartości logicznej:

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

Wartość tej flagi wskazuje obecność lub brak paska postępu w interfejsie.

Wyświetlenia

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Utwórz

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

Pokaż błędy na ekranie

Pokazywanie błędów w interfejsie przypomina wyświetlanie operacji w toku, ponieważ oba można łatwo przedstawić za pomocą wartości logicznych, które oznaczają ich obecność lub brak. Błędy mogą też obejmować powiązany komunikat z powrotem do użytkownika lub powiązane z nim działanie, które ponawia próbę wykonania nieudanej operacji. W związku z tym, gdy trwa ładowanie albo nie wczytuje się, stany błędu mogą być modelowane za pomocą klas danych hostujących metadane odpowiednie do kontekstu błędu.

Przyjrzyjmy się przykładowi z poprzedniej sekcji, w którym podczas pobierania artykułów wyświetlany był pasek postępu. Jeśli ta operacja zakończy się błędem, warto wyświetlić użytkownikowi co najmniej 1 komunikat z informacją, co poszło nie tak.

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

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

Komunikaty o błędach mogą się wtedy wyświetlać w postaci elementów interfejsu, np. pasków powiadomień. Ma to związek ze sposobem tworzenia i wykorzystywania zdarzeń interfejsu, więc odwiedź stronę Zdarzenia interfejsu, aby dowiedzieć się więcej.

Podział na wątki i równoczesność

Wszystkie działania wykonywane w modelu ViewModel powinny być bezpieczne dla głównego – można je wywoływać z wątku głównego. Dzieje się tak, ponieważ warstwy danych i domeny są odpowiedzialne za przenoszenie pracy do innego wątku.

Jeśli model ViewModel wykonuje długotrwałe operacje, jest również odpowiedzialny za przeniesienie tej logiki do wątku w tle. Kotliny to świetny sposób na zarządzanie równoczesnymi operacjami, a komponenty architektury Jetpack zapewniają ich wbudowane wsparcie. Więcej informacji o używaniu współprogramów w aplikacjach na Androida znajdziesz w tym artykule.

Zmiany w nawigacji w aplikacji są często spowodowane emisją podobną do zdarzeń. Na przykład po zalogowaniu się w klasie SignInViewModel klasa UiState może mieć w polu isSignedIn wartość true. Takie aktywatory powinny być wykorzystywane tak samo jak te omówione powyżej w sekcji Stan interfejsu użytkownika w interfejsie z tą różnicą, że implementacja wykorzystania powinna korzystać z komponentu Nawigacja.

Podział

Biblioteka stron docelowych jest wykorzystywana w interfejsie użytkownika z typem PagingData. Element PagingData reprezentuje i zawiera elementy, które mogą się zmieniać w czasie – czyli nie jest to typ stały – nie powinien być reprezentowany w stałym stanie interfejsu. Zamiast tego należy udostępnić go z poziomu obiektu ViewModel niezależnie w oddzielnym strumieniu. Konkretny przykład znajdziesz w ćwiczeniach z programowania na stronie Android Paging.

Animacje

Aby płynne i płynne przejścia nawigacji na najwyższym poziomie były płynne, przed rozpoczęciem animacji poczekaj, aż drugi ekran wczyta dane. Platforma widoku Androida zapewnia punkty zaczepienia umożliwiające opóźnienie przejścia między miejscami docelowymi fragmentów za pomocą interfejsów API postponeEnterTransition() i startPostponedEnterTransition(). Te interfejsy API zapewniają gotowość elementów interfejsu na drugim ekranie (zwykle obraz pobrany z sieci) do wyświetlenia, zanim interfejs użytkownika animuje przejście do tego ekranu. Więcej szczegółów i szczegółów implementacji znajdziesz w przykładzie Android Motion.

Próbki

Poniższe przykłady Google ilustrują użycie warstwy UI. Zapoznaj się z nimi, aby zastosować te wskazówki w praktyce: