Architektura interfejsu Compose

W Compose interfejs jest niezmienny – nie można go zaktualizować po narysowaniu. Możesz kontrolować stan interfejsu. Za każdym razem, gdy stan interfejsu się zmienia, Compose odtwarza te części drzewa interfejsu, które uległy zmianie. Funkcje kompozycyjne mogą akceptować stan i udostępniać zdarzenia. Na przykład funkcja kompozycyjna TextField akceptuje wartość i udostępnia wywołanie zwrotne onValueChange, które prosi moduł obsługi wywołania zwrotnego o zmianę wartości.

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

Ponieważ elementy kompozycyjne akceptują stan i udostępniają zdarzenia, wzorzec jednokierunkowego przepływu danych dobrze pasuje do Jetpack Compose. Ten przewodnik zawiera informacje o tym, jak zaimplementować wzorzec jednokierunkowego przepływu danych w Compose, jak zaimplementować zdarzenia i kontenery stanu oraz jak pracować z ViewModelami w Compose.

Jednokierunkowy przepływ danych

Jednokierunkowy przepływ danych (UDF) to wzorzec projektowy, w którym stan przepływa w dół, a zdarzenia w górę. Dzięki jednokierunkowemu przepływowi danych możesz oddzielić elementy kompozycyjne, które wyświetlają stan w interfejsie, od części aplikacji, które przechowują i zmieniają stan.

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

  1. Zdarzenie: część interfejsu generuje zdarzenie i przekazuje je w górę, np. kliknięcie przycisku przekazywane do ViewModela w celu obsługi, lub zdarzenie jest przekazywane z innych warstw aplikacji, np. informujące o wygaśnięciu sesji użytkownika.
  2. Aktualizuj stan: procedura obsługi zdarzeń może zmienić stan.
  3. Wyświetl stan: kontener stanu przekazuje stan w dół, a interfejs go wyświetla.
Zdarzenia przepływają z interfejsu do obiektu przechowującego stan, a stan przepływa z obiektu przechowującego stan do interfejsu.
Rysunek 1. Jednokierunkowy przepływ danych.

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

  • Testowanie: oddzielenie stanu od interfejsu, który go wyświetla, ułatwia testowanie obu tych elementów w izolacji.
  • Hermetyzacja stanu: ponieważ stan można aktualizować tylko w jednym miejscu i istnieje tylko jedno źródło prawdy dotyczące stanu elementu kompozycyjnego, jest mniej prawdopodobne, że utworzysz błędy spowodowane niespójnymi stanami.
  • Spójność interfejsu: wszystkie aktualizacje stanu są natychmiast odzwierciedlane w interfejsie dzięki użyciu obserwowalnych kontenerów stanu, takich jak StateFlow czy LiveData.

Jednokierunkowy przepływ danych w Jetpack Compose

Elementy kompozycyjne działają na podstawie stanu i zdarzeń. Na przykład element TextField jest aktualizowany tylko wtedy, gdy aktualizowany jest jego parametr value, i udostępnia wywołanie zwrotne onValueChange – zdarzenie, które prosi o zmianę wartości na nową. Compose definiuje obiekt State jako kontener wartości, a zmiany wartości stanu powodują rekompozycję. Stan możesz przechowywać w remember { mutableStateOf(value) } lub rememberSaveable { mutableStateOf(value) } w zależności od tego, jak długo musisz pamiętać wartość.

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 ViewModela lub z elementu kompozycyjnego nadrzędnego. Nie musisz przechowywać jej w obiekcie State, ale musisz zaktualizować wartość, gdy zostanie wywołana funkcja onValueChange.

Definiowanie parametrów kompozycyjnych

Podczas definiowania parametrów stanu elementu kompozycyjnego pamiętaj o tych kwestiach:

  • Jak bardzo element kompozycyjny nadaje się do ponownego użycia i jak jest elastyczny?
  • Jak parametry stanu wpływają na wydajność tego elementu kompozycyjnego?

Aby promować oddzielanie i ponowne użycie, każdy element kompozycyjny powinien zawierać jak najmniej informacji. Na przykład podczas tworzenia elementu kompozycyjnego, który ma zawierać nagłówek artykułu, lepiej jest przekazywać tylko informacje, które mają być wyświetlane, 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 używanie poszczególnych parametrów poprawia też wydajność. Jeśli na przykład News zawiera więcej informacji niż tylko title i subtitle, za każdym razem, gdy nowa instancja News jest przekazywana do Header(news), element kompozycyjny będzie ponownie komponowany, nawet jeśli title i subtitle się nie zmieniły.

Dokładnie rozważ liczbę przekazywanych parametrów. Funkcja z zbyt dużą liczbą parametrów jest mniej ergonomiczna, dlatego w tym przypadku lepiej jest pogrupować je w klasie.

Zdarzenia w Compose

Każde dane wejściowe w aplikacji powinny być reprezentowane jako zdarzenie: dotknięcia, zmiany tekstu, a nawet timery i inne aktualizacje. Ponieważ te zdarzenia zmieniają stan interfejsu, ViewModel powinien je obsługiwać i aktualizować stan interfejsu.

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

W przypadku stanu i lambd procedur obsługi zdarzeń preferuj przekazywanie wartości niezmiennych. Takie podejście ma następujące korzyści:

  • Zwiększasz możliwość ponownego użycia.
  • Sprawdzasz, czy interfejs nie zmienia bezpośrednio wartości stanu.
  • Unikasz problemów z współbieżnością, ponieważ masz pewność, że stan nie jest modyfikowany z innego wątku.
  • Często zmniejszasz złożoność kodu.

Na przykład element kompozycyjny, który akceptuje jako parametry String i lambdę, można wywołać z wielu kontekstów i jest on bardzo przydatny. Załóżmy, że górny pasek aplikacji w Twojej aplikacji zawsze wyświetla tekst i ma przycisk Wstecz. Możesz zdefiniować bardziej ogólny element kompozycyjny MyAppTopAppBar, który jako parametry przyjmuje tekst i procedurę obsługi 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.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModele, stany i zdarzenia – przykład

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

  • Stan interfejsu jest udostępniany za pomocą obserwowalnych kontenerów stanu, takich jak StateFlow czy LiveData.
  • ViewModel obsługuje zdarzenia pochodzące z interfejsu lub innych warstw aplikacji i aktualizuje kontener stanu na podstawie zdarzeń.

Na przykład podczas implementowania ekranu logowania dotknięcie przycisku Zaloguj się powinno spowodować wyświetlenie przez aplikację wskaźnika postępu i wywołanie sieciowe. Jeśli logowanie się powiedzie, aplikacja przejdzie do innego ekranu. W przypadku błędu aplikacja wyświetli pasek powiadomień. Oto jak można modelować stan ekranu i zdarzenie:

Ekran ma 4 stany:

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

Te stany możesz modelować jako klasę sealed. ViewModel udostępnia 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 Compose udostępnia rozszerzenia dla LiveData, Flow i Observable, które umożliwiają zarejestrowanie się jako odbiorca 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:

Przykłady