Tworzenie architektury interfejsu tworzenia wiadomości

Interfejs Compose jest stały – nie można go już zaktualizować. Możesz kontrolować stan interfejsu użytkownika. Za każdym razem, gdy zmieni się stan interfejsu, funkcja Utwórz odtwarza te części drzewa UI, które uległy zmianie. Obiekty kompozycyjne mogą akceptować stan i ujawniać zdarzenia, np. TextField akceptuje wartość i udostępnia wywołanie zwrotne onValueChange, które żąda od modułu obsługi wywołania zwrotnego zmiany wartości.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Ponieważ obiekty kompozycyjne akceptują zdarzenia stanu i ujawniania, jednokierunkowy wzorzec przepływu danych dobrze pasuje do Jetpack Compose. Opisano w nim sposób wdrażania jednokierunkowego wzorca przepływu danych w Compose, jak implementowaniu zdarzeń i stanów oraz posługiwaniu się modelami ViewModel w oknie Compose.

Jednokierunkowy przepływ danych

Jednokierunkowy przepływ danych (UDF) to wzorzec projektowy, w którym stan przebiega w dół, a zdarzenia napływają. Przepływ danych w jednym kierunku pozwala oddzielić elementy kompozycyjne wyświetlające stan w interfejsie od części aplikacji, które przechowują i zmieniają stan.

Pętla aktualizacji UI aplikacji korzystającej z jednokierunkowego przepływu danych wygląda tak:

  • Zdarzenie: część interfejsu użytkownika generuje zdarzenie i przekazuje je wyżej, np. kliknięcie przycisku, które jest przekazywane do modelu ViewModel w celu obsługi, lub zdarzenie jest przekazywane z innych warstw aplikacji, np. wskazujące, że sesja użytkownika wygasła.
  • Stan aktualizacji: moduł obsługi zdarzeń może zmienić stan.
  • Stan wyświetlania: właściciel stanu przekazuje stan, a interfejs go wyświetla.

Rysunek 1. Jednokierunkowy przepływ danych.

Przestrzeganie tego wzorca podczas korzystania z Jetpack Compose ma kilka zalet:

  • Testowanie: stan odłączenia od interfejsu użytkownika, który go wyświetla, ułatwia testowanie obu rodzajów danych.
  • Objaśnienie stanu: ponieważ stan może być aktualizowany tylko w jednym miejscu i istnieje tylko jedno źródło wiarygodnych informacji o stanie funkcji kompozycyjnej, jest mniejsze prawdopodobieństwo, że z powodu niespójnych stanów wystąpią błędy.
  • Spójność interfejsu: wszystkie aktualizacje stanu są natychmiast odzwierciedlane w interfejsie przez korzystanie z obserwowalnych właścicieli stanu, takich jak StateFlow czy LiveData.

Jednokierunkowy przepływ danych w Jetpack Compose

Elementy kompozycyjne działają w zależności od stanu i zdarzeń. Na przykład element TextField jest aktualizowany tylko po zaktualizowaniu parametru value i udostępnia wywołanie zwrotne onValueChange, czyli zdarzenie, które prosi o zmianę wartości na nową. Tworzenie określa obiekt State jako posiadacz wartości, a zmiana wartości stanu wywołuje zmianę kompozycji. W zależności od tego, jak długo chcesz przechowywać ten stan, możesz go przechowywać w elemencie remember { mutableStateOf(value) } lub rememberSaveable { mutableStateOf(value).

Typ wartości elementu kompozycyjnego TextField to String, więc może ona pochodzić z dowolnego miejsca – z zakodowanej na stałe wartości z modelu ViewModel lub przekazywanej z nadrzędnego elementu kompozycyjnego. Nie musisz jej przechowywać w obiekcie State, ale trzeba zaktualizować wartość po wywołaniu metody onValueChange.

Zdefiniuj parametry kompozycyjne

Podczas określania parametrów stanu funkcji kompozycyjnej pamiętaj o tych kwestiach:

  • W jakim stopniu funkcja wielokrotnego użytku i elastyczność są kompozycyjne?
  • Jak parametry stanu wpływają na wydajność tego elementu kompozycyjnego?

Aby umożliwić oddzielenie i ponowne wykorzystanie, każdy element kompozycyjny powinien zawierać jak najmniej informacji. Gdy np. tworzysz kompozycję do przechowywania nagłówka artykułu z wiadomościami, staraj się przekazywać tylko te informacje, które mają być widoczne, a nie cały artykuł:

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

Czasami korzystanie z pojedynczych parametrów poprawia też wydajność – jeśli np. element News zawiera więcej informacji niż tylko parametr title i subtitle, to za każdym razem, gdy do Header(news) zostanie przekazane nowe wystąpienie elementu News, funkcja kompozycyjna utworzy nową kompozycję, nawet jeśli elementy title i subtitle się nie zmieniły.

Zwracaj szczególną uwagę na liczbę przekazywanych parametrów. Funkcja ze zbyt dużą liczbą parametrów obniża jej ergonomię, więc w tym przypadku preferowane jest zgrupowanie ich w klasie.

Zdarzenia w oknie tworzenia wiadomości

Wszystkie dane wejściowe aplikacji powinny być reprezentowane jako zdarzenia: kliknięcia, zmiany tekstu, a nawet minutniki i inne aktualizacje. Ponieważ te zdarzenia zmieniają stan interfejsu użytkownika, to obiekt ViewModel powinien je obsługiwać i aktualizować stan UI.

Warstwa interfejsu nie powinna nigdy zmieniać stanu poza modułem obsługi zdarzeń, ponieważ może to powodować niespójności i błędy w aplikacji.

Preferuj przekazywanie wartości stałych w lambdach stanu i modułów obsługi zdarzeń. Takie podejście ma następujące korzyści:

  • W ten sposób usprawnisz możliwość wielokrotnego wykorzystania.
  • Dopilnuj, aby interfejs użytkownika nie zmieniał bezpośrednio wartości stanu.
  • Pozwala to uniknąć problemów z równoczesnością, ponieważ dba o to, aby stan nie był mutowany z innego wątku.
  • Często pozwala to zmniejszyć złożoność kodu.

Na przykład funkcja kompozycyjna, która akceptuje funkcje String i lambda jako parametry, może być wywoływana z wielu kontekstów i bardzo nadaje się do wielokrotnego użytku. Załóżmy, że na górnym pasku aplikacji zawsze wyświetla się tekst i zawiera przycisk Wstecz. Możesz zdefiniować bardziej ogólny element kompozycyjny MyAppTopAppBar, który odbiera jako parametry tekst i uchwyt przycisku Wstecz:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

Modele ViewModels, stany i zdarzenia: przykład

Korzystając z ViewModel i mutableStateOf, możesz też wprowadzić w swojej aplikacji jednokierunkowy przepływ danych, jeśli spełniony jest jeden z tych warunków:

  • Stan interfejsu jest udostępniany za pomocą obserwowalnych właścicieli stanu, takich jak StateFlow lub LiveData.
  • ViewModel obsługuje zdarzenia pochodzące z interfejsu użytkownika lub innych warstw aplikacji i aktualizuje identyfikator stanu na podstawie zdarzeń.

Gdy np. implementujesz ekran logowania, kliknięcie przycisku Zaloguj się powinno powodować wyświetlanie ikony postępu i wywołanie sieci. Jeśli logowanie się uda, aplikacja przejdzie na inny ekran, a w przypadku błędu pojawi się pasek powiadomień. Oto jak modeluje się stan ekranu i zdarzenie:

Ekran ma 4 stany:

  • Wylogowano: użytkownik, który jeszcze się nie zalogował.
  • W toku: gdy aplikacja próbuje zalogować użytkownika, wykonując wywołanie sieciowe.
  • Błąd: podczas logowania wystąpił błąd.
  • Zalogowany: gdy użytkownik jest zalogowany.

Te stany możesz modelować jako zapieczętowaną klasę. ViewModel podaje stan jako State, ustawia stan początkowy i w razie potrzeby aktualizuje stan. ViewModel obsługuje też zdarzenie logowania, udostępniając metodę onSignIn().

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

Oprócz interfejsu API mutableStateOf funkcja Compose udostępnia rozszerzenia dla LiveData, Flow i Observable, które umożliwiają zarejestrowanie się jako detektor i reprezentowanie wartości jako stanu.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

Więcej informacji

Więcej informacji o architekturze w Jetpack Compose znajdziesz w tych materiałach:

Próbki