Contenitori di stato e stato dell'interfaccia utente

La guida al livello UI descrive il flusso di dati unidirezionale (UDF) come mezzo per produrre e gestire lo stato dell'interfaccia utente per il livello UI.

I dati scorrono in modo unidirezionale dal livello dati alla UI.
Figura 1. Flusso di dati unidirezionale.

Inoltre, mette in evidenza i vantaggi della delega della gestione delle UDF a una classe speciale chiamata state holder. Puoi implementare un gestore dello stato tramite un ViewModel o una classe semplice. Questo documento esamina più da vicino gli state holder e il ruolo che svolgono nel livello UI.

Al termine di questo documento, dovresti avere una comprensione di come gestire lo stato dell'applicazione nel livello UI, ovvero la pipeline di produzione dello stato UI. Devi essere in grado di comprendere e conoscere quanto segue:

  • Comprendi i tipi di stato dell'interfaccia utente esistenti nel livello UI.
  • Comprendere i tipi di logica che operano su questi stati dell'interfaccia utente nel livello UI.
  • Sapere come scegliere l'implementazione appropriata di un segnaposto di stato, ad esempio un ViewModel o una classe.

Elementi della pipeline di produzione dello stato dell'interfaccia utente

Lo stato della UI e la logica che lo produce definiscono il livello UI.

Stato dell'interfaccia utente

UI state è la proprietà che descrive la UI. Esistono due tipi di stato dell'interfaccia utente:

  • Screen UI state (Stato dell'interfaccia utente dello schermo) è ciò che devi visualizzare sullo schermo. Ad esempio, una classe NewsUiState può contenere gli articoli di notizie e altre informazioni necessarie per il rendering dell'interfaccia utente. Questo stato è in genere collegato ad altri livelli della gerarchia perché contiene dati delle app.
  • Lo stato dell'elemento UI si riferisce alle proprietà intrinseche degli elementi UI che influenzano il modo in cui vengono visualizzati. Un elemento della UI può essere mostrato o nascosto e può avere un determinato carattere, dimensione del carattere o colore del carattere. In Android Views, la visualizzazione gestisce questo stato autonomamente, in quanto è intrinsecamente stateful, esponendo metodi per modificare o interrogare il suo stato. Un esempio sono i metodi get e set della classe TextView per il testo. In Jetpack Compose, lo stato è esterno al componibile e puoi persino spostarlo fuori dalla vicinanza immediata del componibile nella funzione componibile chiamante o in un contenitore di stato. Un esempio è ScaffoldState per il componente componibile Scaffold.

Funzione logica

Lo stato dell'interfaccia utente non è una proprietà statica, in quanto i dati dell'applicazione e gli eventi utente causano la modifica dello stato dell'interfaccia utente nel tempo. La logica determina i dettagli della modifica, inclusi quali parti dello stato dell'interfaccia utente sono cambiate, perché sono cambiate e quando dovrebbero cambiare.

La logica produce lo stato della UI
Figura 2. La logica come produttore dello stato dell'interfaccia utente.

La logica in un'applicazione può essere logica di business o logica UI:

  • La logica di business è l'implementazione dei requisiti di prodotto per i dati delle app. Ad esempio, l'aggiunta di un articolo ai preferiti in un'app di lettura di notizie quando l'utente tocca il pulsante. Questa logica per salvare un segnalibro in un file o database viene in genere inserita nei livelli di dominio o dati. Il gestore dello stato in genere delega questa logica a questi livelli chiamando i metodi che espongono.
  • La logica dell'interfaccia utente è correlata a come visualizzare lo stato dell'interfaccia utente sullo schermo. Ad esempio, ottenere il suggerimento giusto per la barra di ricerca quando l'utente ha selezionato una categoria, scorrere fino a un determinato elemento in un elenco o la logica di navigazione fino a una schermata specifica quando l'utente fa clic su un pulsante.

Ciclo di vita di Android e tipi di stato e logica della UI

Il livello UI è composto da due parti: una dipendente e l'altra indipendente dal ciclo di vita della UI. Questa separazione determina le origini dati disponibili per ogni parte e pertanto richiede diversi tipi di stato e logica della UI.

  • Ciclo di vita dell'interfaccia utente indipendente: questa parte del livello dell'interfaccia utente gestisce i livelli di produzione dei dati dell'app (livelli di dati o di dominio) ed è definita dalla logica di business. Il ciclo di vita, le modifiche alla configurazione e la ricreazione Activity nell'interfaccia utente possono influire sull'attivazione della pipeline di produzione dello stato dell'interfaccia utente, ma non influiscono sulla validità dei dati prodotti.
  • Dipendente dal ciclo di vita della UI: questa parte del livello UI gestisce la logica della UI ed è influenzata direttamente dalle modifiche al ciclo di vita o alla configurazione. Queste modifiche influenzano direttamente la validità delle origini dati lette al suo interno e, di conseguenza, il suo stato può cambiare solo quando il suo ciclo di vita è attivo. Esempi includono le autorizzazioni di runtime e l'ottenimento di risorse dipendenti dalla configurazione, come le stringhe localizzate.

Quanto sopra può essere riassunto nella tabella seguente:

Ciclo di vita dell'interfaccia utente indipendente Dipendente dal ciclo di vita della UI
Logica di business UI Logic
Stato dell'interfaccia utente dello schermo

La pipeline di produzione dello stato dell'interfaccia utente

La pipeline di produzione dello stato UI si riferisce ai passaggi intrapresi per produrre lo stato UI. Questi passaggi comprendono l'applicazione dei tipi di logica definiti in precedenza e dipendono completamente dalle esigenze della tua UI. Alcune UI possono trarre vantaggio da parti della pipeline indipendenti e dipendenti dal ciclo di vita della UI, da entrambe o da nessuna.

ovvero sono valide le seguenti permutazioni della pipeline del livello UI:

  • Stato della UI prodotto e gestito dalla UI stessa. Ad esempio, un semplice contatore di base riutilizzabile:

    @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")
            }
        }
    }
    
  • Logica UI → UI. Ad esempio, mostrare o nascondere un pulsante che consente a un utente di andare all'inizio di un elenco.

    @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()
        }
    }
    
  • Logica di business → UI. Un elemento UI che mostra la foto dell'utente corrente sullo schermo.

    @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)
    }
    
  • Logica di business → Logica UI → UI. Un elemento UI che scorre per visualizzare le informazioni corrette sullo schermo per un determinato stato della UI.

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

Nel caso in cui entrambi i tipi di logica vengano applicati alla pipeline di produzione dello stato dell'interfaccia utente, la logica di business deve sempre essere applicata prima della logica dell'interfaccia utente. Se tenti di applicare la logica di business dopo la logica della UI, la logica di business dipende dalla logica della UI. Le sezioni seguenti spiegano perché questo è un problema esaminando in dettaglio i diversi tipi di logica e i relativi detentori dello stato.

I dati fluiscono dal livello di produzione dei dati alla UI
Figura 3. Applicazione della logica nel livello UI.

Parti interessate e relative responsabilità

La responsabilità di un titolare dello stato è quella di memorizzare lo stato in modo che l'app possa leggerlo. Nei casi in cui è necessaria la logica, funge da intermediario e fornisce l'accesso alle origini dati che ospitano la logica richiesta. In questo modo, il titolare dello stato delega la logica all'origine dati appropriata.

Ciò comporta i seguenti vantaggi:

  • Interfacce utente semplici: l'interfaccia utente associa semplicemente il suo stato.
  • Manutenibilità: la logica definita nel gestore dello stato può essere iterata senza modificare l'UI stessa.
  • Testabilità: l'interfaccia utente e la logica di produzione dello stato possono essere testate in modo indipendente.
  • Leggibilità: i lettori del codice possono vedere chiaramente le differenze tra il codice di presentazione dell'interfaccia utente e il codice di produzione dello stato dell'interfaccia utente.

Indipendentemente dalle dimensioni o dall'ambito, ogni elemento dell'interfaccia utente ha una relazione 1:1 con il relativo contenitore di stato. Inoltre, un gestore dello stato deve essere in grado di accettare ed elaborare qualsiasi azione dell'utente che potrebbe comportare una modifica dello stato dell'interfaccia utente e deve produrre la conseguente modifica dello stato.

Tipi di titolari dello stato

Analogamente ai tipi di stato e logica dell'interfaccia utente, esistono due tipi di detentori dello stato nel livello UI definiti dalla loro relazione con il ciclo di vita della UI:

  • Il detentore dello stato della logica di business.
  • Il contenitore di stato della logica dell'interfaccia utente.

Le sezioni seguenti esaminano più da vicino i tipi di state holder, a partire dallo state holder della logica di business.

Logica di business e relativo titolare dello stato

I gestori dello stato della logica di business elaborano gli eventi utente e trasformano i dati dai livelli di dati o di dominio allo stato dell'interfaccia utente della schermata. Per offrire un'esperienza utente ottimale quando si considerano il ciclo di vita di Android e le modifiche alla configurazione dell'app, i gestori dello stato che utilizzano la logica di business devono avere le seguenti proprietà:

Proprietà Dettaglio
Produce lo stato dell'interfaccia utente I titolari dello stato della logica di business sono responsabili della produzione dello stato della UI per le loro UI. Questo stato dell'interfaccia utente è spesso il risultato dell'elaborazione degli eventi utente e della lettura dei dati dai livelli di dominio e dati.
Conservati tramite la ricreazione dell'attività I titolari dello stato della logica di business mantengono il proprio stato e le pipeline di elaborazione dello stato durante la ricreazione di Activity, contribuendo a fornire un'esperienza utente ottimale. Nei casi in cui il detentore dello stato non può essere mantenuto e viene ricreato (di solito dopo l'interruzione del processo), deve essere in grado di ricreare facilmente il suo ultimo stato per garantire un'esperienza utente coerente.
Possedere uno stato di lunga durata I titolari dello stato della logica di business vengono spesso utilizzati per gestire lo stato delle destinazioni di navigazione. Di conseguenza, spesso mantengono il proprio stato durante le modifiche alla navigazione finché non vengono rimossi dal grafico di navigazione.
È univoco per la sua UI e non è riutilizzabile I titolari dello stato della logica di business in genere producono lo stato per una determinata funzione dell'app, ad esempio un TaskEditViewModel o un TaskListViewModel, e pertanto sono applicabili solo a quella funzione dell'app. Lo stesso titolare dello stato può supportare queste funzioni dell'app su diversi fattori di forma. Ad esempio, le versioni per dispositivi mobili, TV e tablet dell'app potrebbero riutilizzare lo stesso holder dello stato della logica di business.

Ad esempio, considera la destinazione di navigazione dell'autore nell'app "Now in Android":

L&#39;app Now in Android mostra come una destinazione di navigazione che rappresenta una funzione principale dell&#39;app debba avere
il proprio stato di logica di business univoco.
Figura 4. L'app Now in Android.

In qualità di detentore dello stato della logica di business, AuthorViewModel produce lo stato dell'interfaccia utente in questo caso:

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

    val uiState: StateFlow<AuthorScreenUiState> = 

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

Tieni presente che AuthorViewModel ha gli attributi descritti in precedenza:

Proprietà Dettaglio
Produce AuthorScreenUiState AuthorViewModel legge i dati da AuthorsRepository e NewsRepository e li utilizza per produrre AuthorScreenUiState. Applica anche la logica aziendale quando l'utente vuole seguire o smettere di seguire un Author delegando l'operazione a AuthorsRepository.
Ha accesso al livello dati Nel costruttore vengono passate un'istanza di AuthorsRepository e NewsRepository, che consentono di implementare la logica di business del follow di un Author.
Sopravvive Activity alla ricreazione Poiché viene implementato con un ViewModel, verrà mantenuto durante la ricreazione rapida di Activity. In caso di interruzione del processo, l'oggetto SavedStateHandle può essere letto per fornire la quantità minima di informazioni necessarie per ripristinare lo stato dell'interfaccia utente dal livello dati.
Possiede uno stato di lunga durata ViewModel è limitato al grafico di navigazione, pertanto, a meno che la destinazione dell'autore non venga rimossa dal grafico di navigazione, lo stato dell'interfaccia utente in uiState StateFlow rimane in memoria. L'utilizzo di StateFlow aggiunge anche il vantaggio di rendere pigra l'applicazione della logica di business che produce lo stato, perché lo stato viene prodotto solo se esiste un raccoglitore dello stato UI.
È univoco per la sua UI AuthorViewModel è applicabile solo alla destinazione di navigazione dell'autore e non può essere riutilizzato altrove. Se esiste una logica di business riutilizzata in più destinazioni di navigazione, questa deve essere incapsulata in un componente con ambito a livello di dati o di dominio.

ViewModel come contenitore dello stato della logica di business

I vantaggi dei ViewModel nello sviluppo Android li rendono adatti a fornire l'accesso alla logica di business e a preparare i dati dell'applicazione per la presentazione sullo schermo. Questi vantaggi includono:

  • Le operazioni attivate dai ViewModel sopravvivono alle modifiche alla configurazione.
  • Integrazione con Navigatore:
    • Navigation memorizza nella cache i ViewModels mentre lo schermo si trova nello stack precedente. È importante per avere i dati caricati in precedenza disponibili immediatamente quando torni a destinazione. È più difficile da fare con un state holder che segue il ciclo di vita della schermata componibile.
    • Anche il ViewModel viene cancellato quando la destinazione viene rimossa dallo stack indietro, garantendo che lo stato venga pulito automaticamente. Questo è diverso dall'ascolto dello smaltimento componibile che può avvenire per diversi motivi, ad esempio il passaggio a una nuova schermata, a causa di una configurazione modificata o altri motivi.
  • Integrazione con altre librerie Jetpack come Hilt.

Logica UI e relativo contenitore di stato

La logica UI è la logica che opera sui dati forniti dalla UI stessa. Ciò può avvenire sullo stato degli elementi dell'interfaccia utente o sulle origini dati dell'interfaccia utente, come l'API Permissions o Resources. I contenitori di stato che utilizzano la logica UI in genere hanno le seguenti proprietà:

  • Produce lo stato dell'interfaccia utente e gestisce lo stato degli elementi dell'interfaccia utente.
  • Non sopravvive alla ricreazione di Activity: i titolari dello stato ospitati nella logica dell'interfaccia utente spesso dipendono dalle origini dati dell'interfaccia utente stessa e il tentativo di conservare queste informazioni in caso di modifiche alla configurazione causa più spesso una perdita di memoria. Se i titolari dello stato hanno bisogno che i dati vengano mantenuti in seguito alle modifiche alla configurazione, devono delegarli a un altro componente più adatto a sopravvivere alla ricreazione di Activity. In Jetpack Compose, ad esempio, gli stati degli elementi dell'interfaccia utente componibili creati con le funzioni remembered spesso vengono delegati a rememberSaveable per preservare lo stato durante la ricreazione di Activity. Esempi di queste funzioni includono rememberScaffoldState() e rememberLazyListState().
  • Ha riferimenti a origini dati con ambito UI: le origini dati come le API e le risorse del ciclo di vita possono essere referenziate e lette in modo sicuro poiché il contenitore dello stato della logica dell'UI ha lo stesso ciclo di vita dell'UI.
  • È riutilizzabile in più interfacce utente: diverse istanze dello stesso contenitore di stato della logica dell'interfaccia utente possono essere riutilizzate in diverse parti dell'app. Ad esempio, un contenitore di stato per la gestione degli eventi di input dell'utente per un gruppo di chip può essere utilizzato in una pagina di ricerca per i chip di filtro e anche per il campo "A" per i destinatari di un'email.

Il titolare dello stato della logica dell'interfaccia utente viene in genere implementato con una classe semplice. Questo perché la UI stessa è responsabile della creazione del contenitore di stato della logica UI e il contenitore di stato della logica UI ha lo stesso ciclo di vita della UI. In Jetpack Compose, ad esempio, il contenitore dello stato fa parte della composizione e segue il ciclo di vita della composizione.

Quanto sopra può essere illustrato nel seguente esempio nell'esempio Now in Android:

Ora in Android utilizza un semplice contenitore di stato della classe per gestire la logica della UI
Figura 5. L'app di esempio Now in Android.

L'esempio Now in Android mostra una barra delle app in basso o una barra di spostamento per la navigazione, a seconda delle dimensioni dello schermo del dispositivo. Gli schermi più piccoli utilizzano la barra delle app in basso, mentre gli schermi più grandi la barra di navigazione.

Poiché la logica per decidere l'elemento UI di navigazione appropriato utilizzato nella funzione componibile NiaApp non dipende dalla logica di business, può essere gestita da un semplice holder di stato della classe chiamato 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) { /* ... */ }

     /* ... */
}

Nell'esempio precedente, i seguenti dettagli relativi a NiaAppState sono notevoli:

  • Non sopravvive alla ricreazione di Activity: NiaAppState è remembered in Composition creandolo con una funzione composable rememberNiaAppState seguendo le convenzioni di denominazione di Compose. Dopo la ricreazione di Activity, l'istanza precedente viene persa e ne viene creata una nuova con tutte le dipendenze passate, adatta alla nuova configurazione di Activity ricreato. Queste dipendenze potrebbero essere nuove o ripristinate dalla configurazione precedente. Ad esempio, rememberNavController() viene utilizzato nel costruttore NiaAppState e delega a rememberSaveable per mantenere lo stato durante la ricreazione di Activity.
  • Contiene riferimenti a origini dati con ambito UI: i riferimenti a navigationController, Resources e altri tipi simili con ambito del ciclo di vita possono essere conservati in modo sicuro in NiaAppState, in quanto condividono lo stesso ambito del ciclo di vita.

Scegliere tra un ViewModel e una classe semplice per un contenitore di stato

Dalle sezioni precedenti, la scelta tra un ViewModel e un semplice holder dello stato della classe dipende dalla logica applicata allo stato dell'interfaccia utente e dalle origini dati su cui opera la logica.

In sintesi, il seguente diagramma mostra la posizione dei titolari dello stato nella pipeline di produzione dello stato dell'interfaccia utente:

I dati vengono trasferiti dal livello di produzione dei dati al livello UI
Figura 6. Contenitori di stato nella pipeline di produzione dello stato UI. Le frecce indicano il flusso di dati.

In definitiva, devi produrre lo stato della UI utilizzando i contenitori di stato più vicini al punto in cui viene utilizzato. In termini meno formali, dovresti mantenere lo stato il più basso possibile, mantenendo la proprietà corretta. Se hai bisogno di accedere alla logica di business e vuoi che lo stato della UI venga mantenuto finché è possibile navigare in una schermata, anche dopo la ricreazione di Activity, un ViewModel è un'ottima scelta per l'implementazione del titolare dello stato della logica di business. Per lo stato della UI e la logica della UI di durata inferiore, è sufficiente una classe semplice il cui ciclo di vita dipende esclusivamente dalla UI.

I contenitori di stato sono componibili

I titolari dello stato possono dipendere da altri titolari dello stato, a condizione che le dipendenze abbiano una durata uguale o inferiore. Alcuni esempi sono:

  • un contenitore di stato della logica UI può dipendere da un altro contenitore di stato della logica UI.
  • un gestore di stato a livello di schermata può dipendere da un gestore di stato della logica UI.

Lo snippet di codice seguente mostra come DrawerState di Compose dipende da un altro gestore di stato interno, SwipeableState, e come un gestore di stato della logica UI di un'app potrebbe dipendere da 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)
}

Un esempio di dipendenza che sopravvive a un contenitore di stato è un contenitore di stato della logica UI che dipende da un contenitore di stato a livello di schermata. In questo modo, la riusabilità del gestore di stato di durata inferiore diminuirebbe e avrebbe accesso a una logica e a uno stato maggiori di quelli di cui ha effettivamente bisogno.

Se il gestore dello stato con durata inferiore ha bisogno di determinate informazioni da un gestore dello stato con ambito più ampio, trasmetti solo le informazioni necessarie come parametro anziché trasmettere l'istanza del gestore dello stato. Ad esempio, nel seguente snippet di codice, la classe di gestione dello stato della logica UI riceve solo ciò di cui ha bisogno come parametri dalla ViewModel, anziché passare l'intera istanza di ViewModel come dipendenza.

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

Il seguente diagramma rappresenta le dipendenze tra la UI e i diversi gestori di stato dello snippet di codice precedente:

UI a seconda del contenitore di stato della logica UI e del contenitore di stato a livello di schermo
Figura 7. UI a seconda dei diversi contenitori di stato. Le frecce indicano le dipendenze.

Campioni

I seguenti esempi di Google mostrano l'utilizzo dei gestori di stato nel livello UI. Esplorali per vedere queste indicazioni in pratica: