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. Il suo scopo è quello di aiutarti a capire quanto segue:
- Quali API utilizzare per produrre lo stato della UI. Dipende dalla natura delle origini della modifica dello stato disponibili nei tuoi gestori dello stato, seguendo i principi del flusso di dati unidirezionale.
- Come definire l'ambito della produzione dello stato dell'UI in modo da tenere conto delle risorse di sistema.
- Come esporre lo stato dell'interfaccia utente per l'utilizzo 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 di Snackbar, 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 che comprende quanto segue:
- Ingressi: le origini del cambio di stato. Possono 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 nello stato dell'interfaccia utente, ad esempio la chiamata del 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 quanto sopra.
- 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 nello stato dell'interfaccia utente, ad esempio la chiamata del metodo
- State holder: tipi che applicano la logica di business e 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 | Utilizza API asincrone come coroutine e flussi per eseguire il lavoro al di fuori del thread dell'interfaccia utente per mantenere la UI priva di jank. |
| Output | Utilizza API di contenitori di dati osservabili come Compose State o StateFlow per invalidare e eseguire nuovamente il rendering dell'UI quando lo stato cambia. I titolari dei dati osservabili assicurano che la UI abbia sempre uno stato da visualizzare sullo schermo. |
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 la seguente:
- Riconoscimento 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 utilizzo: la UI deve essere in grado di eseguire facilmente il rendering dello stato della UI prodotto. In Jetpack Compose, il consumo di stato è fondamentale per la UI, perché i composable possono essere aggiornati in base alle modifiche dello stato.
Input nelle pipeline di produzione dello stato
Gli input in una pipeline di produzione dello stato forniscono le loro origini di modifica dello stato nel seguente modo:
- 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
Gestire lo stato con i contenitori di dati osservabili. Utilizza l'API mutableStateOf,
soprattutto quando lavori con le API Compose text. Per una gestione dello stato più complessa o quando esegui l'integrazione con altri componenti architetturali, utilizza l'API MutableStateFlow. 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 dei dadi. Ogni lancio
dei dadi da parte dell'utente richiama il metodo sincrono Random.nextInt
e il risultato viene scritto nello stato dell'UI.
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
}
}
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,
)
}
}
}
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 dell'UI.
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))
}
}
}
}
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))
}
}
}
}
}
Modifica dello stato dell'interfaccia utente dai thread in background
È preferibile avviare le coroutine sul dispatcher principale per la produzione
dello stato dell'interfaccia utente, ovvero al di fuori del blocco withContext negli snippet di codice riportati di seguito.
Tuttavia, se devi aggiornare lo stato della UI in un contesto di background diverso,
puoi procedere nel seguente modo:
- Utilizza il metodo
withContextper eseguire le coroutine in un contesto concorrente diverso. - Quando utilizzi
MutableStateFlow, utilizza il metodoupdatecome di consueto. - Quando utilizzi Compose State, utilizza il metodo
Snapshot.withMutableSnapshotper garantire aggiornamenti atomici di 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.
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
}
}
}
}
}
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,
)
}
}
}
}
}
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 è visibile nel sample"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 alla UI un controllo più granulare
sull'attività della pipeline di produzione dello stato, in quanto potrebbe dover
essere attiva solo quando la UI è visibile.
- Utilizza
SharingStarted.WhileSubscribedse la pipeline deve essere attiva solo quando la UI è visibile durante la raccolta del flusso in modo consapevole del ciclo di vita. - Utilizza
SharingStarted.Lazilyse la pipeline deve essere attiva finché l'utente potrebbe tornare all'interfaccia utente, ovvero l'interfaccia utente si trova nello stack precedente o in un'altra scheda fuori dallo schermo.
Nei casi in cui l'aggregazione di origini 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 flussi o convoglia il loro output in flussi e riprendi l'elaborazione come descritto nella sezione sui flussi 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.
Prendi in considerazione TaskDetailViewModel dal repository architecture-samples. Lo stato della UI dipende da uno stream per l'attività corrente (_task) e
da un'origine one-shot (_isTaskDeleted) che si aggiorna quando l'attività viene eliminata. Questo
flag è necessario per distinguere i casi in cui un'attività non viene trovata nel
database a causa di un ID errato e i casi in cui non viene trovata perché l'utente
l'ha appena eliminata:
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, taskAsync ->
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
}
}
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, taskAsync ->
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 }
}
}
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 dipendono in gran parte dall'API utilizzata dall'app per il rendering dell'UI, ad esempio Compose. Jetpack Compose è il toolkit moderno consigliato per la creazione di una UI nativa. Le considerazioni da fare includono quanto segue:
- Stato di lettura in modo consapevole del ciclo di vita.
- Indica se esporre lo stato in uno o più campi dal contenitore dello stato.
La seguente tabella riepiloga le API da utilizzare per la pipeline di produzione dello stato quando utilizzi Jetpack Compose:
| Input | Output |
|---|---|
| API one-shot | StateFlow o Scrivi State |
| API Stream | StateFlow |
| API di streaming e one-shot | 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
le risorse di sistema. In pratica, spesso significa attendere che ci sia un
consumer 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:
Risorse aggiuntive
Per saperne di più sullo stato dell'interfaccia utente, consulta le seguenti risorse aggiuntive:
Documentazione
Visualizza contenuti
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}