Le UI moderne sono raramente statiche. Lo stato della UI cambia quando l'utente interagisce con la UI o quando l'app deve mostrare nuovi dati.
Questo documento definisce le linee guida per la produzione e la gestione dello stato dell'interfaccia utente. Al termine devi:
- Sapere quali API dovresti usare per generare lo stato dell'interfaccia utente. Ciò dipende dalla natura delle origini del cambiamento di stato disponibili nei tuoi stati titolari, seguendo i principi del flusso di dati unidirezionale.
- Scopri come definire l'ambito della produzione dello stato dell'interfaccia utente per essere consapevole delle risorse di sistema.
- Scopri come esporre lo stato dell'interfaccia utente per l'utilizzo.
Fondamentalmente, la produzione statale è l'applicazione incrementale di queste modifiche allo stato dell'interfaccia utente. Lo stato esiste sempre e cambia in base agli eventi. Le differenze tra gli eventi e lo stato sono riassunte nella tabella di seguito:
Eventi | Stato |
---|---|
Sono temporanei, imprevedibili e esistono per un periodo limitato. | Esiste sempre. |
Gli input della produzione statale. | L'output della produzione di stato. |
Il prodotto dell'interfaccia utente o di altre origini. | Viene utilizzato dall'interfaccia utente. |
Un ottimo termine mnemonico per riassumere quanto detto è stato è: eventi. Lo schema riportato di seguito consente di visualizzare le modifiche dello stato man mano che si verificano eventi in una sequenza temporale. Ogni evento viene elaborato dal titolare dello stato appropriato e comporta un cambiamento di stato:
Gli eventi possono provenire da:
- Utenti: mentre interagiscono con l'interfaccia utente dell'app.
- Altre origini del cambiamento di stato: API che presentano dati delle app provenienti, rispettivamente, da interfaccia utente, dominio o livello dati, ad esempio eventi di timeout della snackbar, casi d'uso o repository.
La pipeline di produzione dello stato della UI
La produzione statale nelle app Android può essere considerata come una pipeline di elaborazione che comprende:
- Input: le origini della modifica dello stato. ad esempio:
- Locali al livello UI: possono essere eventi utente come un utente che inserisce un titolo per una "da fare" in un'app di gestione delle attività o API che forniscono accesso alla logica UI che guidano le modifiche nello stato dell'interfaccia utente. Ad esempio,
chiamando il metodo
open
suDrawerState
in Jetpack Compose. - Esterni al livello UI: sono origini dal dominio o dai livelli dati che causano modifiche allo stato dell'interfaccia utente. Ad esempio, notizie che hanno terminato il caricamento da un evento
NewsRepository
o da altri eventi. - Una combinazione di tutte le funzionalità precedenti.
- Locali al livello UI: possono essere eventi utente come un utente che inserisce un titolo per una "da fare" in un'app di gestione delle attività o API che forniscono accesso alla logica UI che guidano le modifiche nello stato dell'interfaccia utente. Ad esempio,
chiamando il metodo
- Titolari di stato: tipi che applicano la logica di business e/o la logica UI alle origini dei cambiamenti di stato ed elaborano gli eventi utente per produrre lo stato dell'UI.
- Output: lo stato della UI che può essere visualizzato dall'app per fornire agli utenti le informazioni di cui hanno bisogno.
API di produzione statale
Nella produzione dello stato vengono utilizzate due API principali, a seconda della fase della pipeline in cui ti trovi:
Fase pipeline | API |
---|---|
Ingresso | Devi utilizzare API asincrone per eseguire operazioni sul thread dell'interfaccia utente e mantenere libero il jank della UI. Ad esempio, Coroutines o Flows in Kotlin e RxJava o callback in Java Programming Language. |
Uscita | Devi usare API osservabili dei titolari di dati per invalidare e eseguire il rendering dell'interfaccia utente quando lo stato cambia. Ad esempio StateFlow, Compose State o LiveData. I titolari di dati osservabili garantiscono che l'UI abbia sempre uno stato dell'UI da visualizzare sullo schermo |
Dei 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 indicano il tipo di elaborazione che potrebbe essere applicato alla pipeline.
Assemblaggio della pipeline di produzione statale
Nelle sezioni successive vengono descritte le tecniche di produzione degli stati più adatte per vari input e le API di output corrispondenti. Ogni pipeline di produzione dello stato è una combinazione di input e output e dovrebbe essere:
- Conforme al ciclo di vita: nel caso in cui l'interfaccia utente non sia visibile o attiva, la pipeline di produzione dello stato non deve consumare risorse, a meno che non sia esplicitamente richiesta.
- Facile da utilizzare: l'interfaccia utente deve essere in grado di eseguire facilmente il rendering dello stato dell'interfaccia utente generato. Le considerazioni sull'output della pipeline di produzione dello stato varieranno tra le diverse API View, ad esempio 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 proprie origini di cambiamento di stato tramite:
- Operazioni one-shot che possono essere sincrone o asincrone, ad esempio chiamate alle funzioni
suspend
. - API Stream, ad esempio
Flows
. - Tutte le risposte precedenti.
Le sezioni seguenti spiegano come assemblare una pipeline di produzione dello stato per ciascuno degli input riportati sopra.
API one-shot come origini del cambiamento di stato
Utilizza l'API MutableStateFlow
come container di stato osservabile e modificabile. Nelle app Jetpack Compose, puoi anche prendere in considerazione
mutableStateOf
, soprattutto quando lavori con le
API Compose Text. Entrambe le API offrono metodi che consentono aggiornamenti atomici sicuri dei valori ospitati, indipendentemente dal fatto che gli aggiornamenti siano sincroni o asincroni.
Ad esempio, prendi in considerazione gli aggiornamenti di stato in una semplice app che lancia il dado. Ogni lancio del dado da parte dell'utente richiama il metodo sincrono Random.nextInt()
e il risultato viene scritto nello stato dell'interfaccia utente.
Flusso statale
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 scrittura
@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'interfaccia utente dalle chiamate asincrone
Per cambiamenti di stato che richiedono un risultato asincrono, avvia una Coroutine nell'elemento CoroutineScope
appropriato. In questo modo l'app può eliminare il lavoro quando CoroutineScope
viene annullato. Il titolare dello stato scrive quindi il risultato della chiamata al metodo di sospensione nell'API osservabile utilizzata per esporre lo stato dell'interfaccia utente.
Ad esempio, considera AddEditTaskViewModel
nel campo
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.
Flusso statale
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 scrittura
@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 Coroutines sul supervisore principale
per la produzione dello stato dell'interfaccia utente. Vale a dire, al di fuori del blocco withContext
negli snippet di codice riportati di seguito. Tuttavia, se devi aggiornare lo stato della UI in un contesto in background diverso, puoi farlo utilizzando le API seguenti:
- Utilizza il metodo
withContext
per eseguire le coroutine in un contesto simultaneo diverso. - Quando utilizzi
MutableStateFlow
, usa il metodoupdate
come di consueto. - Quando usi lo stato di composizione, usa
Snapshot.withMutableSnapshot
per garantire gli aggiornamenti atomici allo stato nel contesto simultaneo.
Ad esempio, supponiamo nello snippet DiceRollViewModel
riportato di seguito che SlowRandom.nextInt()
sia una funzione suspend
con elevata intensità di calcolo che deve essere chiamata da una Coroutine associata alla CPU.
Flusso statale
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 scrittura
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
}
}
}
}
}
Trasmetti le API come origini del cambiamento di stato
Per le origini di cambiamento 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 dello stato.
Quando utilizzi Kotlin Flows, puoi ottenere questo risultato con la funzione combine. Un esempio può essere visto nell'esempio "Ora in Android" in InteressiViewModel:
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'uso 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
essere attivo solo quando la UI è visibile.
- Utilizza
SharingStarted.WhileSubscribed()
se la pipeline deve essere attiva solo quando l'UI è visibile durante la raccolta del flusso in un modo sensibile al ciclo di vita. - Utilizza
SharingStarted.Lazily
se la pipeline deve essere attiva, purché l'utente possa tornare all'interfaccia utente, ovvero se la UI si trova nel backstack o in un'altra scheda fuori schermo.
Nei casi in cui l'aggregazione di origini di stato basate su flussi non sia applicabile, le API per i flussi come Kotlin Flows offrono un'ampia gamma di trasformazioni come unione, scomposizione e così via per facilitare l'elaborazione dei flussi nello stato dell'interfaccia utente.
API one-shot e stream come origini del cambiamento di stato
Nel caso in cui la pipeline di produzione dello stato dipenda sia dalle chiamate one-shot sia dai flussi di dati come origini di modifica dello stato, i flussi sono il vincolo che lo definisce. Di conseguenza, converti le chiamate one-shot in API per i flussi oppure pubblica l'output in flussi e riprendi l'elaborazione come descritto nella precedente sezione relativa ai flussi.
Con i flussi, in genere ciò significa creare una o più istanze MutableStateFlow
di supporto privato per propagare le modifiche di stato. Puoi anche creare flussi di snapshot dallo stato Scrivi.
Considera TaskDetailViewModel
del repository
architecture-samples di seguito:
Flusso statale
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 scrittura
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'interfaccia utente e la natura della sua presentazione dipendono in gran parte dall'API utilizzata dall'app per il rendering dell'interfaccia utente. Nelle app per Android, puoi scegliere di usare Views o Jetpack Compose. Ecco alcune considerazioni da fare:
- Lettura dello stato consapevole del ciclo di vita.
- Indica se lo stato deve essere esposto in uno o più campi dal titolare dello stato.
La tabella seguente riassume le API da utilizzare per la pipeline di produzione statale per ogni input e consumer specifico:
Ingresso | Consumismo | Uscita |
---|---|---|
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 one-shot e stream | Visualizzazioni | StateFlow o LiveData |
API one-shot e stream | Scrivi | StateFlow |
Inizializzazione della pipeline di produzione statale
L'inizializzazione delle pipeline di produzione dello stato implica l'impostazione delle condizioni iniziali per l'esecuzione della pipeline. Potresti dover fornire valori di input iniziali
fondamentali per l'avvio della pipeline, ad esempio un valore id
per la
visualizzazione dettagliata di un articolo o l'avvio di un caricamento asincrono.
Dovresti inizializzare la pipeline di produzione dello stato in modo lento, quando possibile, per conservare le risorse di sistema.
In pratica, questo spesso significa attendere fino a quando non ci sarà un consumatore dell'output. Le API Flow
consentono questa operazione con l'argomento started
nel metodo stateIn
. Nei casi in cui non è applicabile,
definisci una funzione idempotente
initialize()
per avviare esplicitamente la pipeline di produzione dello stato,
come mostrato nello snippet seguente:
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
}
}
}
Samples
I seguenti esempi di Google mostrano la produzione dello stato nel livello UI. Esplorale per vedere concretamente queste indicazioni:
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Livello UI
- Creare un'app offline
- Titolari stati e stato dell'interfaccia utente {:#mad-arch}