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.
Inoltre, mette in evidenza i vantaggi della delega della gestione delle UDF a una classe speciale
chiamata contenitore di stato. Puoi implementare un contenitore di 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 della UI. Devi essere in grado di comprendere e conoscere quanto segue:
- Comprendi i tipi di stato della UI 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 contenitore di stato, ad esempio un
ViewModelo una classe.
Elementi della pipeline di produzione dello stato dell'interfaccia utente
Lo stato dell'UI e la logica che lo produce definiscono il livello UI.
Stato dell'UI
UI state è la proprietà che descrive l'interfaccia utente. 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
NewsUiStatepuò contenere gli articoli di notizie e altre informazioni necessarie per il rendering della UI. Questo stato è in genere collegato ad altri livelli della gerarchia perché contiene dati delle app. - Lo stato dell'elemento dell'interfaccia utente si riferisce alle proprietà intrinseche degli elementi dell'interfaccia utente che
influenzano il modo in cui vengono visualizzati. Un elemento UI può essere mostrato o nascosto e può
avere un determinato carattere, dimensione del carattere o colore del carattere. In Jetpack
Compose, lo stato è esterno al componibile e puoi anche spostarlo
dalla vicinanza immediata del componibile alla funzione componibile chiamante
o a un contenitore di stato. Un esempio è
ScaffoldStateper il composableScaffold.
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 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 contenitore di 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 di un elenco o la logica di navigazione 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 dell'interfaccia utente. 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 UI 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
Activitynell'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. Alcuni 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'UI 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 dell'interfaccia utente si riferisce ai passaggi intrapresi per produrre lo stato dell'interfaccia utente. 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 contenitori di stato.
Titolari dello stato e loro responsabilità
La responsabilità di un contenitore di 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 contenitore di 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 contenitore di 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 contenitore di 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, nel livello UI esistono due tipi di detentori dello stato definiti dalla loro relazione con il ciclo di vita dell'interfaccia utente:
- Il contenitore di stato della logica di business.
- Il contenitore di stato della logica dell'interfaccia utente.
Le sezioni seguenti esaminano più da vicino i tipi di contenitore di stato, a partire dal contenitore di stato della logica di business.
Logica di business e relativo contenitore di stato
I contenitori di 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à | Dettagli |
|---|---|
| Produce lo stato dell'UI | 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 contenitori di 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 contenitore di 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 loro 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 contenitore di 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 contenitore di stato della logica di business. |
Ad esempio, considera la destinazione di navigazione dell'autore nell'app "Now in Android":
In qualità di contenitore di 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à | Dettagli |
|---|---|
Produce AuthorScreenUiState |
AuthorViewModel legge i dati da AuthorsRepository e NewsRepository e li utilizza per produrre AuthorScreenUiState. Applica anche la logica di business quando l'utente vuole seguire o non seguire più 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 di 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 ViewModel mentre lo schermo si trova nel back stack. È importante per avere i dati caricati in precedenza disponibili immediatamente quando torni a destinazione. È più difficile da fare con un contenitore di stato che segue il ciclo di vita della schermata del componibile.
- La ViewModel viene cancellata anche quando la destinazione viene rimossa dallo stack indietro, garantendo che lo stato venga pulito automaticamente. Questo è diverso dall'ascolto dello smaltimento componibile che può verificarsi per diversi motivi, ad esempio il passaggio a una nuova schermata, a causa di una configurazione modificata o per altri motivi.
- Integrazione con altre librerie Jetpack come Hilt.
Logica dell'interfaccia utente e relativo contenitore di stato
La logica della UI è una 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: i contenitori di stato ospitati nella logica dell'interfaccia utente spesso dipendono dalle origini dati dell'interfaccia utente stessa e il tentativo di conservare queste informazioni in seguito a modifiche alla configurazione causa più spesso una perdita di memoria.
ActivitySe 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 diActivity. In Jetpack Compose, ad esempio, gli stati degli elementi UI componibili creati con le funzionirememberedspesso vengono delegati arememberSaveableper conservare lo stato durante la ricreazione diActivity. Esempi di queste funzioni includonorememberScaffoldState()erememberLazyListState(). - 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 contenitore di stato della logica dell'interfaccia utente viene in genere implementato con una classe semplice. Questo perché l'UI stessa è responsabile della creazione del contenitore di stato della logica dell'UI e il contenitore di stato della logica dell'UI ha lo stesso ciclo di vita dell'UI. In Jetpack Compose, ad esempio, il contenitore di 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:
L'esempio Now in Android mostra una barra dell'app in basso o una barra di navigazione 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 contenitore 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èrememberednella composizione creandolo con una funzione componibilerememberNiaAppStateseguendo le convenzioni di denominazione di Compose. Dopo la ricreazione diActivity, l'istanza precedente viene persa e ne viene creata una nuova con tutte le dipendenze passate, adatta alla nuova configurazione diActivityricreato. Queste dipendenze potrebbero essere nuove o ripristinate dalla configurazione precedente. Ad esempio,rememberNavController()viene utilizzato nel costruttoreNiaAppStatee delega arememberSaveableper preservare lo stato durante la ricreazione diActivity. - Contiene riferimenti a origini dati con ambito UI: i riferimenti a
navigationController,Resourcese altri tipi simili con ambito del ciclo di vita possono essere conservati in modo sicuro inNiaAppState, 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 contenitore di 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:
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 contenitore di stato della logica di business. Per lo stato della UI e la logica della UI di durata inferiore, dovrebbe essere 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 contenitore di stato a livello di schermata può dipendere da un contenitore di stato della logica UI.
Il seguente snippet di codice mostra come ComposeDrawerState dipende da
un altro contenitore di stato interno, SwipeableState, e come un contenitore 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. Ciò ridurrebbe la riusabilità del contenitore di stato di durata inferiore e gli darebbe accesso a una logica e a uno stato maggiori di quelli di cui ha effettivamente bisogno.
Se il contenitore di stato con durata inferiore ha bisogno di determinate informazioni da un contenitore di stato con ambito superiore, passa solo le informazioni necessarie come parametro anziché passare l'istanza del contenitore di stato. Ad esempio, nel seguente snippet di codice, la classe del contenitore di 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 mostra le dipendenze tra la UI e i diversi detentori dello stato dello snippet di codice precedente:
Esempi
I seguenti esempi di Google mostrano l'utilizzo dei gestori di stato nel livello UI. Esplorali per vedere queste indicazioni in pratica:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Livello UI
- Produzione stato UI
- Guida all'architettura delle app