Le UI moderne sono raramente statiche. Lo stato della UI cambia quando l'utente interagisce con la UI o quando l'app deve visualizzare nuovi dati.
Questo documento definisce le linee guida per la produzione e la gestione dello stato dell'interfaccia utente. Al termine, dovresti:
- Scopri quali API devi utilizzare per produrre lo stato della UI. Ciò dipende dalla natura delle origini del cambiamento di stato disponibili nei tuoi titolari dello stato, seguendo i principi del flusso di dati unidirezionale.
- Scopri come definire l'ambito della produzione dello stato dell'interfaccia utente per tenere conto delle risorse di sistema.
- Scopri come esporre lo stato dell'interfaccia utente per il consumo da parte dell'interfaccia utente.
Fondamentalmente, la produzione dello stato è l'applicazione incrementale di queste modifiche allo stato della UI. Lo stato esiste sempre e cambia in seguito agli eventi. Le differenze tra eventi e stato sono riassunte nella tabella seguente:
| Eventi | Stato |
|---|---|
| Transitori, imprevedibili ed esistenti per un periodo di tempo finito. | Esiste sempre. |
| Gli input della produzione statale. | L'output della produzione dello stato. |
| Il prodotto dell'interfaccia utente o di altre fonti. | Viene utilizzato dalla UI. |
Un ottimo mnemonico che riassume quanto sopra è lo stato è; gli eventi si verificano. Il diagramma seguente aiuta a visualizzare le modifiche dello stato man mano che si verificano gli eventi in una cronologia. Ogni evento viene elaborato dal contenitore di stato appropriato e comporta una modifica dello stato:
Gli eventi possono provenire da:
- Utenti: mentre interagiscono con la UI dell'app.
- Altre origini di modifica dello stato: API che presentano dati delle app da UI, domini o livelli di dati come eventi di timeout della barra delle notifiche, casi d'uso o repository rispettivamente.
La pipeline di produzione dello stato dell'interfaccia utente
La produzione di stati nelle app per Android può essere considerata una pipeline di elaborazione composta da:
- Ingressi: le origini del cambio di stato. Potrebbero essere:
- Locali al livello UI: potrebbero essere eventi utente come l'inserimento di un titolo per un'attività in un'app di gestione delle attività o API che forniscono l'accesso alla logica dell'interfaccia utente che determina le modifiche allo stato dell'interfaccia utente. Ad esempio,
chiamando il metodo
opensuDrawerStatein Jetpack Compose. - Esterno al livello UI: si tratta di origini dei livelli di dominio o dati
che causano modifiche allo stato della UI. Ad esempio, notizie che hanno terminato
il caricamento da un
NewsRepositoryo altri eventi. - Un mix di tutti i precedenti.
- Locali al livello UI: potrebbero essere eventi utente come l'inserimento di un titolo per un'attività in un'app di gestione delle attività o API che forniscono l'accesso alla logica dell'interfaccia utente che determina le modifiche allo stato dell'interfaccia utente. Ad esempio,
chiamando il metodo
- State holder: tipi che applicano la logica di business e/o la logica dell'interfaccia utente alle origini del cambiamento di stato ed elaborano gli eventi utente per produrre lo stato dell'interfaccia utente.
- Output: lo stato dell'interfaccia utente che l'app può visualizzare per fornire agli utenti le informazioni di cui hanno bisogno.
API di produzione dello stato
Esistono due API principali utilizzate nella produzione di stati a seconda della fase della pipeline in cui ti trovi:
| Fase della pipeline | API |
|---|---|
| Input | Devi utilizzare le API asincrone per eseguire il lavoro al di fuori del thread UI per evitare problemi di prestazioni dell'interfaccia utente. Ad esempio, coroutine o flussi in Kotlin e RxJava o callback nel linguaggio di programmazione Java. |
| Output | Devi utilizzare le API observable data holder per invalidare e eseguire nuovamente il rendering della UI quando lo stato cambia. Ad esempio, StateFlow, Compose State o LiveData. I contenitori di dati osservabili garantiscono che la UI abbia sempre uno stato UI da visualizzare sullo schermo |
Tra le due, la scelta dell'API asincrona per l'input ha una maggiore influenza sulla natura della pipeline di produzione dello stato rispetto alla scelta dell'API osservabile per l'output. Questo perché gli input determinano il tipo di elaborazione che può essere applicato alla pipeline.
Assemblaggio della pipeline di produzione dello stato
Le sezioni successive trattano le tecniche di produzione dello stato più adatte a vari input e le API di output corrispondenti. Ogni pipeline di produzione dello stato è una combinazione di input e output e deve essere:
- Rilevamento del ciclo di vita: nel caso in cui la UI non sia visibile o attiva, la pipeline di produzione dello stato non deve consumare risorse a meno che non sia esplicitamente richiesto.
- Facilità di lettura: la UI deve essere in grado di eseguire facilmente il rendering dello stato della UI prodotto. Le considerazioni per l'output della pipeline di produzione dello stato variano a seconda delle diverse API View, come il sistema View o Jetpack Compose.
Input nelle pipeline di produzione dello stato
Gli input in una pipeline di produzione dello stato possono fornire le loro origini di modifica dello stato tramite:
- Operazioni una tantum che possono essere sincrone o asincrone, ad esempio
chiamate alle funzioni
suspend. - API di streaming, ad esempio
Flows. - Tutte le risposte precedenti.
Le sezioni seguenti descrivono come assemblare una pipeline di produzione di stati per ciascuno degli input precedenti.
API one-shot come origini di modifica dello stato
Utilizza l'API MutableStateFlow come contenitore osservabile e modificabile
dello stato. Nelle app Jetpack Compose, puoi anche prendere in considerazione
mutableStateOf, soprattutto quando lavori con le
API di testo di Compose. Entrambe le API offrono metodi che consentono aggiornamenti atomici sicuri ai valori che ospitano, indipendentemente dal fatto che gli aggiornamenti siano sincroni o asincroni.
Ad esempio, considera gli aggiornamenti dello stato in una semplice app per il lancio di dadi. Ogni lancio di dadi da parte dell'utente richiama il metodo sincrono Random.nextInt() e il risultato viene scritto nello stato dell'interfaccia utente.
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Stato di composizione
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
Modifica dello stato dell'UI dalle chiamate asincrone
Per le modifiche dello stato che richiedono un risultato asincrono, avvia una coroutine nel
CoroutineScope appropriato. In questo modo l'app può ignorare il lavoro quando il
CoroutineScope viene annullato. Il contenitore di stato scrive quindi il risultato della chiamata al metodo suspend nell'API osservabile utilizzata per esporre lo stato dell'UI.
Ad esempio, considera AddEditTaskViewModel nell'esempio di architettura. Quando il metodo saveTask() di sospensione
salva un'attività in modo asincrono, il metodo update su
MutableStateFlow propaga la modifica dello stato allo stato della UI.
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Stato di composizione
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
Modifica dello stato dell'interfaccia utente dai thread in background
È preferibile avviare le coroutine sul dispatcher principale per la produzione
dello stato dell'UI. ovvero al di fuori del blocco withContext negli snippet di codice
di seguito. Tuttavia, se devi aggiornare lo stato dell'interfaccia utente in un contesto
di background diverso, puoi farlo utilizzando le seguenti API:
- Utilizza il metodo
withContextper eseguire le coroutine in un contesto simultaneo diverso. - Quando utilizzi
MutableStateFlow, utilizza il metodoupdatecome di consueto. - Quando utilizzi Compose State, utilizza
Snapshot.withMutableSnapshotper garantire aggiornamenti atomici a State nel contesto simultaneo.
Ad esempio, supponiamo che nello snippet DiceRollViewModel riportato di seguito, SlowRandom.nextInt() sia una funzione suspend ad alta intensità di calcolo che deve essere chiamata da una coroutine associata alla CPU.
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
Stato di composizione
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
API di streaming come origini di modifica dello stato
Per le origini del cambio di stato che producono più valori nel tempo nei flussi, l'aggregazione degli output di tutte le origini in un insieme coeso è un approccio semplice alla produzione di stati.
Quando utilizzi Kotlin Flows, puoi farlo con la funzione combine. Un esempio può essere visto nell'esempio "Now in Android" in InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
L'utilizzo dell'operatore stateIn per creare StateFlows offre all'interfaccia utente un controllo più granulare sull'attività della pipeline di produzione dello stato, in quanto potrebbe essere necessario che sia attiva solo quando l'interfaccia utente è visibile.
- Utilizza
SharingStarted.WhileSubscribed()se la pipeline deve essere attiva solo quando l'interfaccia utente è visibile durante la raccolta del flusso in modo consapevole del ciclo di vita. - Utilizza
SharingStarted.Lazilyse la pipeline deve essere attiva finché l'utente può tornare all'interfaccia utente, ovvero se l'interfaccia utente si trova nello stack precedente o in un'altra scheda fuori dallo schermo.
Nei casi in cui l'aggregazione delle origini di stato basate su stream non si applica, le API di stream come Kotlin Flows offrono un ricco insieme di trasformazioni come unione, appiattimento e così via per facilitare l'elaborazione degli stream nello stato della UI.
API one-shot e di streaming come origini di modifica dello stato
Nel caso in cui la pipeline di produzione dello stato dipenda sia da chiamate one-shot sia da stream come fonti di modifica dello stato, gli stream sono il vincolo determinante. Pertanto, converti le chiamate one-shot in API di stream o convoglia il loro output in stream e riprendi l'elaborazione come descritto nella sezione precedente.
Con i flussi, in genere significa creare una o più istanze di backend privato
MutableStateFlow per propagare le modifiche dello stato. Puoi anche creare flussi di snapshot dallo stato di composizione.
Considera il TaskDetailViewModel dal repository
architecture-samples riportato di seguito:
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Stato di composizione
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
Tipi di output nelle pipeline di produzione dello stato
La scelta dell'API di output per lo stato dell'UI e la natura della sua presentazione dipende in gran parte dall'API utilizzata dall'app per il rendering dell'UI. Nelle app per Android, puoi scegliere di utilizzare Views o Jetpack Compose. Le considerazioni da fare includono:
- Stato di lettura in modo consapevole del ciclo di vita.
- Indica se lo stato deve essere esposto in uno o più campi dal contenitore di stato.
La tabella seguente riepiloga le API da utilizzare per la pipeline di produzione dello stato per un determinato input e consumer:
| Input | Consumer | Output |
|---|---|---|
| API one-shot | Visualizzazioni | StateFlow o LiveData |
| API one-shot | Scrivi | StateFlow o Scrivi State |
| API Stream | Visualizzazioni | StateFlow o LiveData |
| API Stream | Scrivi | StateFlow |
| API di streaming e one-shot | Visualizzazioni | StateFlow o LiveData |
| API di streaming e one-shot | Scrivi | StateFlow |
Stato dell'inizializzazione della pipeline di produzione
L'inizializzazione delle pipeline di produzione dello stato comporta l'impostazione delle condizioni iniziali
per l'esecuzione della pipeline. Ciò potrebbe comportare la fornitura di valori di input iniziali
fondamentali per l'avvio della pipeline, ad esempio un id per la
visualizzazione dettagliata di un articolo di notizie o l'avvio di un caricamento asincrono.
Se possibile, inizializza la pipeline di produzione dello stato in modo differito
per risparmiare risorse di sistema.
In pratica, spesso significa attendere finché non ci sia un consumatore dell'output. Le API Flow lo consentono con l'argomento
started nel metodo stateIn. Nei casi in cui ciò non è applicabile,
definisci una funzione idempotente
initialize() per avviare esplicitamente la pipeline di produzione dello stato
come mostrato nel seguente snippet:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Esempi
I seguenti esempi di Google mostrano la produzione dello 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
- Creare un'app offline-first
- State holder e stato dell'interfaccia utente {:#mad-arch}