Posiadacze stanów i stan interfejsu użytkownika

W przewodniku po warstwie interfejsu omawiamy jednokierunkowy przepływ danych (UDF) jako sposób tworzenia stanu interfejsu i zarządzania nim w warstwie interfejsu.

Dane przepływają jednokierunkowo z warstwy danych do interfejsu.
Rysunek 1. Jednokierunkowy przepływ danych.

Podkreśla też korzyści z przekazywania zarządzania funkcjami UDF do specjalnej klasy zwanej obiektem stanu. Możesz zaimplementować obiekt przechowujący stan za pomocą ViewModel lub zwykłej klasy. W tym dokumencie przyjrzymy się bliżej obiektom stanu i ich roli w warstwie interfejsu.

Po przeczytaniu tego dokumentu będziesz wiedzieć, jak zarządzać stanem aplikacji w warstwie interfejsu, czyli jak działa potok produkcji stanu interfejsu. Powinieneś rozumieć i znać te kwestie:

  • Poznaj typy stanów interfejsu, które występują w warstwie interfejsu.
  • Poznaj rodzaje logiki, które działają w tych stanach interfejsu w warstwie interfejsu.
  • Wiedzieć, jak wybrać odpowiednią implementację obiektu stanu, np. ViewModel lub klasę.

Elementy potoku produkcyjnego stanu interfejsu

Warstwa interfejsu jest określana przez stan interfejsu i logikę, która go tworzy.

Stan interfejsu

Stan interfejsu to właściwość opisująca interfejs. Istnieją 2 rodzaje stanu interfejsu:

  • Stan interfejsu ekranu to to, co musisz wyświetlić na ekranie. Na przykład klasa NewsUiState może zawierać artykuły i inne informacje potrzebne do renderowania interfejsu. Ten stan jest zwykle powiązany z innymi warstwami hierarchii, ponieważ zawiera dane aplikacji.
  • Stan elementu interfejsu odnosi się do właściwości wbudowanych w elementy interfejsu, które wpływają na sposób ich renderowania. Element interfejsu może być wyświetlany lub ukrywany i może mieć określoną czcionkę, rozmiar czcionki lub kolor czcionki. W Android Views stan jest zarządzany przez sam widok, ponieważ jest on z natury stanowy. Udostępnia metody modyfikowania stanu i wysyłania zapytań o niego. Przykładem są metody getset klasy TextView dla tekstu. W Jetpack Compose stan jest zewnętrzny w stosunku do funkcji kompozycyjnej. Możesz go nawet przenieść poza bezpośrednie sąsiedztwo funkcji kompozycyjnej do wywołującej funkcji kompozycyjnej lub do obiektu przechowującego stan. Przykładem jest ScaffoldState w przypadku funkcji kompozycyjnej Scaffold.

Operatory logiczne

Stan interfejsu nie jest właściwością statyczną, ponieważ dane aplikacji i zdarzenia użytkownika powodują, że stan interfejsu zmienia się z czasem. Logika określa szczegóły zmiany, w tym które części stanu interfejsu uległy zmianie, dlaczego i kiedy powinny się zmienić.

Logika generuje stan interfejsu
Rysunek 2. Logika jako producent stanu interfejsu.

Logika w aplikacji może być logiką biznesową lub logiką interfejsu:

  • Logika biznesowa to wdrożenie wymagań dotyczących produktu w odniesieniu do danych aplikacji. Na przykład dodanie artykułu do zakładek w aplikacji do czytania wiadomości, gdy użytkownik kliknie przycisk. Logika zapisywania zakładki w pliku lub bazie danych jest zwykle umieszczana w warstwach domeny lub danych. Obiekt zarządzający stanem zwykle deleguje tę logikę do tych warstw, wywołując udostępniane przez nie metody.
  • Logika interfejsu jest związana z tym, jak wyświetlać stan interfejsu na ekranie. Na przykład uzyskanie odpowiedniej podpowiedzi na pasku wyszukiwania, gdy użytkownik wybierze kategorię, przewinięcie do określonego elementu na liście lub logika nawigacji do określonego ekranu, gdy użytkownik kliknie przycisk.

Cykl życia Androida oraz rodzaje stanu i logiki interfejsu

Warstwa interfejsu użytkownika składa się z 2 części: jednej zależnej od cyklu życia interfejsu użytkownika i drugiej niezależnej od niego. Ten podział określa źródła danych dostępne dla każdej części, a co za tym idzie, wymaga różnych typów stanu i logiki interfejsu.

  • Niezależna od cyklu życia interfejsu: ta część warstwy interfejsu użytkownika zajmuje się warstwami aplikacji, które generują dane (warstwy danych lub domeny), i jest zdefiniowana przez logikę biznesową. Cykl życia, zmiany konfiguracji i Activity odtwarzanie w interfejsie użytkownika mogą wpływać na to, czy potok produkcji stanu interfejsu użytkownika jest aktywny, ale nie wpływają na ważność wygenerowanych danych.
  • Zależna od cyklu życia interfejsu: ta część warstwy interfejsu użytkownika zajmuje się logiką interfejsu i jest bezpośrednio zależna od cyklu życia lub zmian konfiguracji. Te zmiany mają bezpośredni wpływ na ważność źródeł danych odczytywanych w ramach tego zasobu, dlatego jego stan może się zmieniać tylko wtedy, gdy jego cykl życia jest aktywny. Przykłady takich sytuacji to uprawnienia w czasie działania i pobieranie zasobów zależnych od konfiguracji, takich jak zlokalizowane ciągi znaków.

Powyższe informacje podsumowuje tabela poniżej:

Niezależny od cyklu życia interfejsu Zależne od cyklu życia interfejsu
Logika biznesowa Logika interfejsu
Stan interfejsu ekranu

Potok produkcji stanu interfejsu

Proces produkcji stanu interfejsu odnosi się do czynności podejmowanych w celu wygenerowania stanu interfejsu. Te kroki obejmują zastosowanie typów logiki zdefiniowanych wcześniej i są całkowicie zależne od potrzeb interfejsu. Niektóre interfejsy mogą korzystać z części potoku niezależnych od cyklu życia interfejsu i zależnych od niego, z jednej z nich lub z żadnej z nich.

Oznacza to, że prawidłowe są te permutacje potoku warstwy interfejsu:

  • Stan interfejsu użytkownika jest tworzony i zarządzany przez sam interfejs. Na przykład prosty licznik podstawowy, którego można używać wielokrotnie:

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • Logika interfejsu → interfejs. Może to być np. wyświetlanie lub ukrywanie przycisku, który umożliwia użytkownikowi przejście na początek listy.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • Logika biznesowa → interfejs. Element interfejsu wyświetlający zdjęcie bieżącego użytkownika na ekranie.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • Logika biznesowa → logika interfejsu → interfejs. Element interfejsu, który przewija się, aby wyświetlić odpowiednie informacje na ekranie w danym stanie interfejsu.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

Jeśli oba rodzaje logiki są stosowane w procesie generowania stanu interfejsu, logika biznesowa musi być zawsze stosowana przed logiką interfejsu. Próba zastosowania logiki biznesowej po logice interfejsu użytkownika oznaczałaby, że logika biznesowa zależy od logiki interfejsu użytkownika. W kolejnych sekcjach wyjaśnimy, dlaczego jest to problem, przyglądając się szczegółowo różnym typom logiki i ich elementom przechowującym stan.

Dane przepływają z warstwy generującej dane do interfejsu.
Rysunek 3. Zastosowanie logiki w warstwie interfejsu.

Zmienne stanu i ich obowiązki

Zadaniem obiektu stanu jest przechowywanie stanu, aby aplikacja mogła go odczytywać. W przypadku, gdy potrzebna jest logika, działa jako pośrednik i zapewnia dostęp do źródeł danych, które ją zawierają. W ten sposób podmiot przechowujący stan deleguje logikę do odpowiedniego źródła danych.

Daje to następujące korzyści:

  • Proste interfejsy: interfejs wiąże tylko swój stan.
  • Łatwość konserwacji: logikę zdefiniowaną w obiekcie stanu można iteracyjnie ulepszać bez zmiany samego interfejsu.
  • Możliwość testowania: interfejs i logika generowania jego stanu mogą być testowane niezależnie.
  • Czytelność: osoby czytające kod mogą wyraźnie zobaczyć różnice między kodem prezentacji interfejsu a kodem produkcyjnym stanu interfejsu.

Niezależnie od rozmiaru i zakresu każdy element interfejsu ma relację 1:1 z odpowiednim obiektem przechowującym stan. Ponadto element przechowujący stan musi być w stanie przyjmować i przetwarzać wszelkie działania użytkownika, które mogą spowodować zmianę stanu interfejsu, oraz musi generować wynikającą z nich zmianę stanu.

Rodzaje podmiotów państwowych

Podobnie jak w przypadku rodzajów stanu i logiki interfejsu, w warstwie interfejsu użytkownika istnieją 2 typy elementów przechowujących stan, które różnią się relacją z cyklem życia interfejsu:

  • Obiekt przechowujący stan logiki biznesowej.
  • Zmienna stanu logiki interfejsu.

W kolejnych sekcjach przyjrzymy się bliżej typom obiektów stanu, zaczynając od obiektu stanu logiki biznesowej.

Logika biznesowa i jej stan

Obiekty stanu logiki biznesowej przetwarzają zdarzenia użytkownika i przekształcają dane z warstw danych lub domeny w stan interfejsu ekranu. Aby zapewnić optymalną wygodę użytkowników, biorąc pod uwagę cykl życia Androida i zmiany konfiguracji aplikacji, obiekty stanu korzystające z logiki biznesowej powinny mieć te właściwości:

Właściwość Szczegóły
Tworzy stan interfejsu Obiekty stanu logiki biznesowej są odpowiedzialne za generowanie stanu interfejsu. Ten stan interfejsu jest często wynikiem przetwarzania zdarzeń użytkownika i odczytywania danych z warstw domeny i danych.
Zachowywane podczas ponownego tworzenia aktywności Obiekty stanu logiki biznesowej zachowują swój stan i potoki przetwarzania stanu podczas Activity ponownego tworzenia, co pomaga zapewnić użytkownikom wygodę. W przypadkach, gdy nie można zachować obiektu stanu i jest on ponownie tworzony (zwykle po zakończeniu procesu), musi on być w stanie łatwo odtworzyć swój ostatni stan, aby zapewnić spójność wrażeń użytkownika.
Posiadanie stanu długotrwałego Obiekty przechowujące stan logiki biznesowej są często używane do zarządzania stanem miejsc docelowych nawigacji. Dzięki temu często zachowują swój stan podczas zmian nawigacji, dopóki nie zostaną usunięte z grafu nawigacji.
Jest unikalny dla interfejsu i nie można go użyć ponownie. Obiekty przechowujące stan logiki biznesowej zwykle generują stan dla określonej funkcji aplikacji, np. TaskEditViewModel lub TaskListViewModel, i dlatego mają zastosowanie tylko do tej funkcji. Ten sam obiekt stanu może obsługiwać te funkcje aplikacji na różnych urządzeniach. Na przykład wersje aplikacji na urządzenia mobilne, telewizory i tablety mogą ponownie wykorzystywać ten sam stan logiki biznesowej.

Weźmy na przykład miejsce docelowe nawigacji autora w aplikacji „Now in Android”:

Aplikacja Now in Android pokazuje, jak miejsce docelowe nawigacji reprezentujące główną funkcję aplikacji powinno mieć własny, niepowtarzalny stan logiki biznesowej.
Rysunek 4. Aplikacja Now in Android.

AuthorViewModel, które pełni rolę stanu logiki biznesowej, generuje w tym przypadku stan interfejsu:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = 

    // Business logic
    fun followAuthor(followed: Boolean) {
      
    }
}

Zwróć uwagę, że element AuthorViewModel ma atrybuty wymienione wcześniej:

Właściwość Szczegóły
Tworzy AuthorScreenUiState AuthorViewModel odczytuje dane z AuthorsRepositoryNewsRepository i wykorzystuje je do generowania AuthorScreenUiState. Stosuje też logikę biznesową, gdy użytkownik chce obserwować lub przestać obserwować Author, przekazując to zadanie do AuthorsRepository.
Ma dostęp do warstwy danych W konstruktorze przekazywane są do niego instancje AuthorsRepositoryNewsRepository, co umożliwia mu implementację logiki biznesowej związanej z obserwowaniem Author.
Przetrwa Activity odtworzenie Ponieważ jest ona wdrażana za pomocą ViewModel, będzie zachowywana podczas szybkiego ponownego tworzenia Activity. W przypadku zakończenia procesu można odczytać obiekt SavedStateHandle, aby uzyskać minimalną ilość informacji potrzebną do przywrócenia stanu interfejsu z warstwy danych.
Posiada stan długotrwały ViewModel jest ograniczony do grafu nawigacji, więc dopóki miejsce docelowe autora nie zostanie usunięte z grafu nawigacji, stan interfejsu w uiState StateFlow pozostaje w pamięci. Użycie StateFlow ma też tę zaletę, że sprawia, że zastosowanie logiki biznesowej, która generuje stan, jest leniwe, ponieważ stan jest generowany tylko wtedy, gdy istnieje odbiorca stanu interfejsu.
jest unikalny w interfejsie, AuthorViewModel dotyczy tylko miejsca docelowego nawigacji autora i nie można go ponownie wykorzystać w innym miejscu. Jeśli w miejscach docelowych nawigacji jest używana ta sama logika biznesowa, musi ona być zamknięta w komponencie o zakresie danych lub warstwy domeny.

ViewModel jako kontener stanu logiki biznesowej

Zalety ViewModels w programowaniu na Androida sprawiają, że nadają się one do zapewniania dostępu do logiki biznesowej i przygotowywania danych aplikacji do wyświetlania na ekranie. Niektóre z nich to:

  • Operacje wywoływane przez obiekty ViewModel przetrwają zmiany konfiguracji.
  • Integracja z Nawigacją:
    • Biblioteka Navigation przechowuje obiekty ViewModel w pamięci podręcznej, gdy ekran znajduje się na liście wstecznej. Jest to ważne, aby wcześniej wczytane dane były natychmiast dostępne po powrocie do miejsca docelowego. Jest to trudniejsze w przypadku obiektu przechowującego stan, który jest zgodny z cyklem życia komponentu.
    • ViewModel jest też usuwany, gdy miejsce docelowe jest usuwane ze stosu wstecznego, co zapewnia automatyczne czyszczenie stanu. Różni się to od nasłuchiwania usunięcia elementu kompozycyjnego, które może nastąpić z różnych powodów, np. przejścia do nowego ekranu, zmiany konfiguracji lub innych przyczyn.
  • Integracja z innymi bibliotekami Jetpack, takimi jak Hilt.

Logika interfejsu i zmienna stanu

Logika interfejsu to logika, która działa na danych dostarczanych przez sam interfejs. Może to być stan elementów interfejsu lub źródła danych interfejsu, takie jak interfejs API uprawnień lub Resources. Zmienne stanu, które wykorzystują logikę interfejsu, mają zwykle te właściwości:

  • Generuje stan interfejsu i zarządza stanem elementów interfejsu.
  • Nie przetrwa ponownego utworzenia: Activity obiekty stanu hostowane w logice interfejsu często zależą od źródeł danych z samego interfejsu, a próba zachowania tych informacji w przypadku zmian konfiguracji zwykle powoduje wyciek pamięci. Jeśli komponenty przechowujące stan muszą zachowywać dane po zmianach konfiguracji, muszą przekazywać je do innego komponentu, który lepiej radzi sobie z Activity ponownym tworzeniem. Na przykład w Jetpack Compose stany elementów interfejsu utworzone za pomocą funkcji remembered często przekazują stan do rememberSaveable, aby zachować go podczas ponownego tworzenia Activity. Przykłady takich funkcji to rememberScaffoldState()rememberLazyListState().
  • Zawiera odwołania do źródeł danych o zakresie interfejsu: można bezpiecznie odwoływać się do źródeł danych, takich jak interfejsy API cyklu życia i zasoby, oraz je odczytywać, ponieważ element przechowujący stan logiki interfejsu ma taki sam cykl życia jak interfejs.
  • Można go używać w wielu interfejsach: różnych instancji tego samego elementu logicznego interfejsu można używać w różnych częściach aplikacji. Na przykład element do zarządzania zdarzeniami związanymi z działaniami użytkownika w grupie elementów może być używany na stronie wyszukiwania w przypadku elementów filtru, a także w polu „Do” w przypadku odbiorców e-maila.

Stan logiki interfejsu użytkownika jest zwykle implementowany za pomocą zwykłej klasy. Dzieje się tak, ponieważ interfejs użytkownika odpowiada za utworzenie zmiennej stanu logiki interfejsu, a zmienna stanu logiki interfejsu ma taki sam cykl życia jak sam interfejs. Na przykład w Jetpack Compose obiekt przechowujący stan jest częścią kompozycji i podlega jej cyklowi życia.

Powyższe można zilustrować na przykładzie z przykładowej aplikacji Now in Android:

Aplikacja Now in Android używa zwykłej klasy do przechowywania stanu, aby zarządzać logiką interfejsu
Rysunek 5. Przykładowa aplikacja Now in Android.

Przykładowa aplikacja Now in Android wyświetla pasek aplikacji u dołu lub kolumnę nawigacji w zależności od rozmiaru ekranu urządzenia. Na mniejszych ekranach używany jest dolny pasek aplikacji, a na większych – panel nawigacyjny.

Logika decydująca o odpowiednim elemencie interfejsu nawigacji używanym w funkcji kompozycyjnej NiaApp nie zależy od logiki biznesowej, więc można nią zarządzać za pomocą zwykłego obiektu stanu klasy o nazwie NiaAppState:

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

W powyższym przykładzie warto zwrócić uwagę na te szczegóły dotyczące NiaAppState:

  • Nie przetrwa ponownego utworzenia Activity: NiaAppState jest remembered w kompozycji, ponieważ jest tworzony za pomocą funkcji kompozycyjnej rememberNiaAppState zgodnie z konwencjami nazewnictwa Compose. Po ponownym utworzeniu elementu Activity poprzednia instancja zostanie utracona, a nowa instancja zostanie utworzona ze wszystkimi przekazanymi zależnościami, odpowiednimi dla nowej konfiguracji ponownie utworzonego elementu Activity. Te zależności mogą być nowe lub przywrócone z poprzedniej konfiguracji. Na przykład w konstruktorze NiaAppState używany jest element rememberNavController(), który deleguje do rememberSaveable, aby zachować stan podczas odtwarzania Activity.
  • Zawiera odwołania do źródeł danych o zakresie interfejsu: odwołania do navigationController, Resources i innych podobnych typów o zakresie cyklu życia można bezpiecznie przechowywać w NiaAppState, ponieważ mają one ten sam zakres cyklu życia.

Wybór między ViewModel a zwykłą klasą jako elementem przechowującym stan

Z poprzednich sekcji wynika, że wybór między ViewModel a zwykłą klasą przechowującą stan zależy od logiki zastosowanej do stanu interfejsu i źródeł danych, na których ta logika działa.

Podsumowując, poniższy diagram pokazuje pozycję elementów przechowujących stan w interfejsie użytkownika Potok tworzenia stanu:

Dane przepływają z warstwy generującej dane do warstwy interfejsu.
Rysunek 6. Zmienne stanu w potoku produkcji stanu interfejsu. Strzałki oznaczają przepływ danych.

Stan interfejsu należy generować za pomocą obiektów stanu znajdujących się najbliżej miejsca, w którym jest on używany. Mówiąc mniej formalnie, stan powinien być jak najniższy przy zachowaniu odpowiedniej własności. Jeśli potrzebujesz dostępu do logiki biznesowej i chcesz, aby stan interfejsu utrzymywał się tak długo, jak długo można przejść do ekranu, nawet po Activity ponownym utworzeniu, ViewModel to świetny wybór na potrzeby implementacji stanu logiki biznesowej. W przypadku krótszego stanu interfejsu i logiki interfejsu wystarczy zwykła klasa, której cykl życia zależy wyłącznie od interfejsu.

Zmienne stanu można łączyć

Obiekty stanu mogą zależeć od innych obiektów stanu, o ile zależności mają równy lub krótszy czas życia. Przykłady:

  • zmienna stanu logiki interfejsu może zależeć od innej zmiennej stanu logiki interfejsu.
  • zmienna stanu na poziomie ekranu może zależeć od zmiennej stanu logiki interfejsu.

Ten fragment kodu pokazuje, jak DrawerState w Compose zależy od innego wewnętrznego obiektu przechowującego stan, SwipeableState, oraz jak obiekt przechowujący stan logiki interfejsu aplikacji może zależeć od DrawerState:

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

Przykładem zależności, która jest ważniejsza od zmiennej stanu, jest zmienna stanu logiki interfejsu, która zależy od zmiennej stanu na poziomie ekranu. Zmniejszyłoby to możliwość ponownego użycia komponentu stanu o krótszym czasie życia i dałoby mu dostęp do większej liczby funkcji i stanów, niż jest to w rzeczywistości potrzebne.

Jeśli obiekt stanu o krótszym czasie życia potrzebuje określonych informacji z obiektu stanu o szerszym zakresie, przekaż tylko te informacje jako parametr, zamiast przekazywać instancję obiektu stanu. Na przykład w poniższym fragmencie kodu klasa przechowująca stan logiki interfejsu otrzymuje z obiektu ViewModel tylko to, czego potrzebuje, zamiast przekazywać całą instancję obiektu ViewModel jako zależność.

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

Poniższy diagram przedstawia zależności między interfejsem a różnymi elementami przechowującymi stan w poprzednim fragmencie kodu:

Interfejs użytkownika zależny od zmiennej stanu logiki interfejsu i zmiennej stanu na poziomie ekranu
Rysunek 7. interfejsu użytkownika w zależności od różnych zmiennych stanu. Strzałki oznaczają zależności.

Próbki

Poniższe przykłady Google pokazują, jak używać obiektów stanu w warstwie interfejsu. Zapoznaj się z nimi, aby zobaczyć te wskazówki w praktyce: