Les interfaces utilisateur modernes sont rarement statiques. L'état de l'interface utilisateur change lorsque l'utilisateur interagit avec ou lorsque l'application doit afficher de nouvelles données.
Ce document présente diverses consignes concernant la production et la gestion de l'état de l'interface utilisateur. Il est destiné à vous aider à comprendre les points suivants :
- les API à utiliser pour produire l'état de l'interface utilisateur (varie selon la nature des sources de changement d'état disponibles dans vos conteneurs d'état, conformément aux principes du flux de données unidirectionnel) ;
- comment déterminer la portée de la production de l'état de l'interface utilisateur pour tenir compte des ressources système ;
- comment exposer l'état de l'interface utilisateur utilisé par l'UI.
Fondamentalement, la production d'états consiste en l'application progressive de ces modifications à l'état de l'interface utilisateur. L'état existe toujours et change en fonction des événements. Le tableau ci-dessous récapitule les différences entre les événements et les états :
| Événements | État |
|---|---|
| Temporaire, imprévisible et existe pour une durée limitée. | Existe toujours. |
| Entrées de la production d'état. | Résultat de la production d'état. |
| Produit de l'interface utilisateur ou d'autres sources. | Est utilisée par l'UI. |
Pour résumer, un état est, un événement se produit. Le schéma ci-dessous permet de visualiser la façon dont un état change à mesure que des événements se produisent. Chaque événement est traité par le conteneur d'état approprié et entraîne un changement d'état :
Les événements peuvent provenir des sources suivantes :
- Utilisateurs : lorsqu'ils interagissent avec l'interface utilisateur de l'application.
- Autres sources de changement d'état : API qui présentent des données d'application à partir des couches de l'interface utilisateur, de domaine ou de données, comme les événements de délai d'inactivité de la snackbar, les cas d'utilisation ou les dépôts, respectivement.
Pipeline qui génère l'état de l'UI
La production d'état dans les applications Android peut être vue comme un pipeline de traitement comprenant les éléments suivants :
- Entrées : les sources du changement d'état. Elles peuvent être :
- locales, au niveau de l'interface utilisateur : il peut s'agir d'événements utilisateur, par exemple la saisie par l'utilisateur d'un
titre pour une tâche à effectuer dans une application de gestion des tâches, ou d'API donnant
accès à une logique d'interface utilisateur qui entraînent des changements de l'état de l'interface utilisateur, par exemple
l'appel de la méthode
opensurDrawerStatedans Jetpack Compose ; - externes à la couche d'interface utilisateur : il s'agit des sources provenant de la couche de domaine ou de données qui entraînent des changements de l'état de l'interface utilisateur, par exemple des actualités qui ont fini de se charger à partir d'un
NewsRepositoryou d'autres événements ; - un mélange de ces sources.
- locales, au niveau de l'interface utilisateur : il peut s'agir d'événements utilisateur, par exemple la saisie par l'utilisateur d'un
titre pour une tâche à effectuer dans une application de gestion des tâches, ou d'API donnant
accès à une logique d'interface utilisateur qui entraînent des changements de l'état de l'interface utilisateur, par exemple
l'appel de la méthode
- Conteneurs d'état : types qui appliquent une logique métier et une logique d'interface utilisateur aux sources de changement d'état, et qui traitent les événements utilisateur pour générer l'état de l'interface utilisateur.
- Résultat : état de l'interface utilisateur que l'application peut afficher pour fournir aux utilisateurs les informations dont ils ont besoin.
API de production d'état
Deux API principales sont utilisées pour la production d'état, en fonction de l'étape du pipeline :
| Étape du pipeline | API |
|---|---|
| Entrée | Utilisez des API asynchrones telles que les coroutines et les flux pour effectuer des tâches en dehors du thread UI pour que l'interface utilisateur ne présente pas d'à-coups. |
| Sortie | Utilisez des API de conteneurs de données observables telles que Compose State ou StateFlow pour invalider et réafficher l'interface utilisateur lorsque l'état change. Les conteneurs de données observables permettent de s'assurer que l'UI a toujours un état à afficher à l'écran. |
Des deux, choisir l'API asynchrone pour les entrées impacte davantage la nature du pipeline de production d'état que choisir l'API observable pour la sortie. En effet, les entrées dictent le type de traitement qui peut être appliqué au pipeline.
Assemblage du pipeline de production d'état
Les sections suivantes présentent les techniques de production d'état les plus adaptées à différentes entrées et les API de sortie correspondantes. Chaque pipeline de production d'état est une combinaison d'entrées et de sorties et doit être :
- sensible au cycle de vie : dans le cas où l'interface utilisateur n'est pas visible ou active, le pipeline de production d'état ne doit consommer aucune ressource, sauf si cela est explicitement requis ;
- facile à utiliser : l'interface utilisateur doit pouvoir facilement afficher l'état de l'interface utilisateur produit. Dans Jetpack Compose, la consommation d'état est essentielle pour l'interface utilisateur, car les composables peuvent être mis à jour en fonction des changements d'état.
Entrées des pipelines de production d'état
Les entrées d'un pipeline de production d'état fournissent leurs sources de changement d'état via :
- des opérations ponctuelles, qui peuvent être synchrones ou asynchrones, par exemple des appels aux fonctions
suspend; - des API de flux, par exemple
Flows; - toutes les propositions ci-dessus.
Les sections suivantes expliquent comment assembler un pipeline de production d'état pour chacune des entrées ci-dessus.
API ponctuelles comme sources de changement d'état
Gérez l'état avec des conteneurs de données observables. Utilisez l'API mutableStateOf,
en particulier lorsque vous utilisez les API textuelles de Compose. Pour une gestion d'état plus complexe ou lors de l'intégration à d'autres composants d'architecture, utilisez l'
MutableStateFlow API. Les deux API proposent des méthodes permettant de mettre à jour de manière sécurisée et atomique les valeurs qu'elles hébergent, de façon synchrone ou asynchrone.
Prenons l'exemple de mises à jour d'état dans une application simple de lancer de dés. Chaque lancer de
dés de l'utilisateur appelle la méthode synchrone Random.nextInt method,
et le résultat est écrit dans l'état de l'interface utilisateur.
État de Compose
@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,
)
}
}
}
Modification de l'état de l'interface utilisateur à partir d'appels asynchrones
Pour les changements d'état qui nécessitent un résultat asynchrone, lancez une coroutine dans le CoroutineScope approprié. L'application peut ainsi supprimer le travail lorsque le CoroutineScope est annulé. Le conteneur d'état écrit ensuite le résultat de l'appel de méthode de suspension dans l'API observable utilisée pour fournir l'état de l'interface utilisateur.
Prenons l'exemple de AddEditTaskViewModel dans l'
exemple d'architecture. Lorsque la méthode de suspension saveTask enregistre une tâche
de manière asynchrone, la méthode update sur MutableStateFlow propage le
changement d'état à l'état de l'interface utilisateur.
État de Compose
@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))
}
}
}
}
}
Modification de l'état de l'interface utilisateur à partir de threads en arrière-plan
Il est préférable de lancer des coroutines sur le coordinateur principal pour la production de l'état de l'interface utilisateur, c'est-à-dire en dehors du bloc withContext dans les extraits de code ci-dessous.
Toutefois, si vous devez mettre à jour l'état de l'interface utilisateur dans un autre contexte d'arrière-plan, vous pouvez procéder comme suit :
- Utilisez la
withContextméthode pour exécuter des coroutines dans un autre contexte simultané. - Lorsque vous utilisez
MutableStateFlow, utilisez laupdateméthode comme d'habitude. - Lorsque vous utilisez l'état Compose, utilisez la
Snapshot.withMutableSnapshotméthode pour garantir la mise à jour atomique de l'état dans le contexte simultané.
Par exemple, supposons que, dans l'extrait DiceRollViewModel ci-dessous, SlowRandom.nextInt soit une fonction suspend qui consomme beaucoup de ressources de calcul et doive être appelée à partir d'une coroutine liée au processeur.
État de Compose
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 de flux comme sources de changement d'état
Pour les sources de changement d'état qui produisent plusieurs valeurs au fil du temps dans des flux, une approche simple pour produire des états consiste à agréger les sorties de toutes les sources dans un ensemble cohérent.
Si vous utilisez des flux Kotlin, vous pouvez vous servir de la fonction combine.
Vous trouverez un exemple de cela dans l'exemple "Now in Android" dans le
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'utilisation de l'opérateur stateIn pour créer des StateFlows permet à l'interface utilisateur de contrôler plus précisément l'activité du pipeline de production d'état. En effet, il se peut qu'il doive être actif uniquement lorsque l'interface utilisateur est visible.
- Utilisez
SharingStarted.WhileSubscribedsi le pipeline ne doit être actif que lorsque l'interface utilisateur est visible, tout en collectant le flux en tenant compte du cycle de vie. - Utilisez
SharingStarted.Lazilysi le pipeline doit être actif tant que l'utilisateur peut revenir à l'interface utilisateur, c'est-à-dire si celle-ci se trouve sur la pile "Retour" ou dans un autre onglet hors écran.
Dans les cas où l'agrégation de sources d'état basées sur des flux ne s'applique pas, les API de flux telles que les flux Kotlin offrent un ensemble riche de transformations, comme la fusion, l'aplatissement, etc. pour traiter plus facilement les flux dans l'état de l'interface utilisateur.
API ponctuelles et de flux comme sources de changement d'état
Dans le cas où le pipeline de production d'état repose à la fois sur des appels ponctuels et des flux comme sources de changement d'état, les flux constituent la contrainte clé. Par conséquent, convertissez les appels ponctuels en API de flux ou agrégez leur sortie en flux, puis reprenez le traitement comme décrit dans la section "Flux" ci-dessus.
Dans le cas des flux, cela implique généralement de créer une ou plusieurs instances de sauvegarde privées MutableStateFlow pour propager les changements d'état. Vous pouvez également créer
des flux d'instantanés à partir de l'état de Compose.
Examinez le TaskDetailViewModel du architecture-samples
dépôt. L'état de l'interface utilisateur dépend d'un flux pour la tâche actuelle (_task) et d'une source ponctuelle (_isTaskDeleted) qui se met à jour lorsque la tâche est supprimée. Cet indicateur est nécessaire pour faire la distinction entre le moment où une tâche est introuvable dans la base de données en raison d'un ID incorrect et le moment où elle est introuvable, car l'utilisateur vient de la supprimer :
État de Compose
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 }
}
}
Types de sorties dans les pipelines de production d'état
Le choix de l'API de sortie pour l'état de l'interface utilisateur et la nature de sa présentation dépendent en grande partie de l'API dont votre application se sert pour afficher l'interface utilisateur, comme Compose. Jetpack Compose est le kit d'outils moderne recommandé pour créer des UI natives. Les considérations incluent ici :
- la lecture de l'état en tenant compte du cycle de vie ;
- la nécessité ou non d'afficher l'état dans un ou plusieurs champs à partir du conteneur d'état.
Le tableau suivant présente les API à utiliser pour votre pipeline de production d'état lorsque vous utilisez Jetpack Compose :
| Entrée | Sortie |
|---|---|
| API ponctuelles | StateFlow ou Compose State |
| API de flux | StateFlow |
| API ponctuelles et de flux | StateFlow |
Initialisation du pipeline de production d'état
L'initialisation du pipeline de production d'état implique de définir les conditions initiales d'exécution du pipeline. Cela peut nécessiter de fournir des valeurs d'entrée initiales critiques pour le démarrage du pipeline, par exemple un id pour la vue détaillée d'un article d'actualités, ou de démarrer un chargement asynchrone.
Si possible, initialisez le pipeline de production d'état en différé afin de conserver les ressources système. Dans la pratique, cela implique souvent d'attendre qu'il y ait un consommateur pour la sortie. Les API Flow autorisent cette opération avec l'argument started
dans la méthode stateIn. Dans les cas non applicables, définissez
une fonction idempotente initialize pour démarrer explicitement le pipeline de production d'état
, comme indiqué dans l'extrait suivant :
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
}
}
}
Exemples
Les exemples Google suivants illustrent la génération d'un état dans la couche d'interface utilisateur. Parcourez-les pour voir ces conseils en pratique :
Ressources supplémentaires
Pour en savoir plus sur l'état de l'interface utilisateur, consultez les ressources supplémentaires suivantes :
Documentation
Afficher le contenu
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Couche d'interface utilisateur
- Créer une application orientée hors connexion
- Conteneurs d'état et état de l'interface utilisateur {:#mad-arch}