Tworzenie architektury interfejsu tworzenia wiadomości

W sekcji „Tworzenie” interfejs jest niezmienny – nie można go zaktualizować po jego narysowaniu. Możesz kontrolować stan interfejsu użytkownika. Za każdym razem, gdy stan interfejsu użytkownika ulegnie zmianie, Compose ponownie tworzy te części drzewa interfejsu, które uległy zmianie. Komponenty mogą przyjmować stan i wyświetlać zdarzenia. Na przykład komponent TextField może przyjmować wartość i wyświetlać funkcję wywołania zwrotnego onValueChange, która prosi moduł obsługi funkcji wywołania zwrotnego o zmianę wartości.

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

Ponieważ komponenty przyjmują stan i wyświetlają zdarzenia, wzór jednokierunkowego przepływu danych dobrze pasuje do Jetpack Compose. W tym przewodniku znajdziesz informacje o tym, jak w Compose zaimplementować wzór jednokierunkowego przepływu danych, jak wdrażać zdarzenia i holdery stanu oraz jak pracować z ViewModelami w Compose.

Jednokierunkowy przepływ danych

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

Cykl aktualizacji interfejsu użytkownika w przypadku 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 przekazane do obsługi do ViewModel. Zdarzenie może też być przekazywane z innych warstw aplikacji, np. informujące o wygaśnięciu sesji użytkownika.
  2. Aktualizowanie stanu: może go zmienić moduł obsługi zdarzenia.
  3. Wyświetlanie stanu: obiekt stanu przekazuje stan, a interfejs użytkownika go wyświetla.

Zdarzenia przepływają z interfejsu do uchwytu stanu, a stan przepływa z uchwytu stanu do interfejsu.
Rysunek 1. Jednokierunkowy przepływ danych.

Stosowanie tego schematu podczas korzystania z Jetpack Compose daje kilka korzyści:

  • Możliwość testowania: odłączenie stanu od interfejsu, który go wyświetla, ułatwia testowanie obu elementów oddzielnie.
  • Opakowanie stanu: stan można zaktualizować tylko w jednym miejscu, a stan kompozytu ma tylko jedno źródło informacji, więc tworzenie błędów z powodu niezgodnych stanów jest mniej prawdopodobne.
  • Spójność interfejsu: wszystkie aktualizacje stanu są natychmiast odzwierciedlane w interfejsie dzięki zastosowaniu obserwowalnych uchwytów stanu, takich jak StateFlow lub LiveData.

Jednokierunkowy przepływ danych w Jetpack Compose

Elementy składane działają na podstawie stanu i zdarzeń. Na przykład obiekt TextField jest aktualizowany tylko wtedy, gdy zostanie zaktualizowany parametr value, i wyświetla wywołanie zwrotne onValueChange, czyli zdarzenie, które prosi o zmianę wartości na nową. Składnik Compose definiuje obiekt State jako uchwyt wartości, a zmiany wartości stanu powodują ponowne skompilowanie. 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 komponentu TextField to String, więc może ona pochodzić z dowolnego miejsca: z wartości zakodowanej na stałe, z ViewModel lub przekazana przez nadrzędny komponent. Nie musisz przechowywać go w obiekcie State, ale musisz zaktualizować wartość, gdy wywoływana jest funkcja onValueChange.

Definiowanie parametrów kompozytowych

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

  • Jak często można używać danego elementu i jak jest on elastyczny?
  • Jak parametry stanu wpływają na działanie tego komponentu?

Aby zachęcić do rozdzielania i wielokrotnego używania, każdy element powinien zawierać jak najmniej informacji. Na przykład podczas tworzenia kompozytowego komponentu do wyświetlania nagłówka artykułu z wiadomościami warto przekazać tylko te 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życie poszczególnych parametrów też poprawia wydajność. Jeśli na przykład News zawiera więcej informacji niż tylko titlesubtitle, za każdym razem, gdy do Header(news) zostanie przekazany nowy egzemplarz News, kompozyt zostanie ponownie skompilowany, nawet jeśli titlesubtitle się nie zmieniły.

Uważnie rozważ liczbę parametrów, które przekazujesz. Funkcja z zbyt dużą liczbą parametrów zmniejsza ergonomię, dlatego w tym przypadku zaleca się grupowanie ich w klasie.

Wydarzenia w edytorze

Każde działanie użytkownika w aplikacji powinno być reprezentowane jako zdarzenie: kliknięcia, zmiany tekstu, a nawet zegary lub inne aktualizacje. Ponieważ te zdarzenia zmieniają stan interfejsu użytkownika, ViewModel powinien je obsługiwać i aktualizować stan interfejsu.

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

W przypadku funkcji lambda stanu i obsługi zdarzeń preferuj przekazywanie wartości niezmiennych. Ta metoda ma te zalety:

  • zwiększa możliwość ponownego użycia.
  • Upewnij się, że interfejs użytkownika 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 zmniejsza to złożoność kodu.

Na przykład kompozyt, który przyjmuje jako parametry String i funkcję lambda, może być wywoływany z wielu kontekstów i jest bardzo wielokrotnego użytku. Załóżmy, że górny pasek aplikacji zawsze wyświetla tekst i przycisk Wstecz. Możesz zdefiniować bardziej ogólny komponent MyAppTopAppBar, który otrzymuje tekst i identyfikator przycisku Wstecz jako parametry:

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

ViewModels, stany i zdarzenia: przykład

Korzystając z elementów ViewModelmutableStateOf, możesz też wprowadzić w aplikacji jednokierunkowy przepływ danych, jeśli spełniony jest co najmniej 1 z tych warunków:

  • Stan interfejsu użytkownika jest widoczny za pomocą obserwowalnych uchwytów stanu, takich jak StateFlow lub LiveData.
  • ViewModel obsługuje zdarzenia pochodzące z interfejsu użytkownika lub innych warstw aplikacji i aktualizuje stan zgodnie z tymi zdarzeniami.

Na przykład podczas implementowania ekranu logowania kliknięcie przycisku Zaloguj się powinno spowodować wyświetlenie w aplikacji wskaźnika postępu i wywołanie połączenia z siecią. Jeśli udało się zalogować, aplikacja przejdzie do innego ekranu. W przypadku błędu wyświetli się pasek informacji. Oto jak modelować stan ekranu i zdarzenie:

Ekran ma 4 stany:

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

Te stany możesz modelować jako zamkniętą klasę. ViewModel udostępnia stan jako State, ustawia stan początkowy i aktualizuje go w miarę potrzeby. 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 zawiera rozszerzenia dla funkcji LiveData, Flow i Observable, które rejestrują się jako słuchacze i reprezentują wartość jako stan.

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