Couche de l'interface utilisateur

Le rôle de l'UI est d'afficher les données de l'application à l'écran et de servir de point principal d'interaction utilisateur. Chaque fois que les données changent, que ce soit du fait d'une interaction de l'utilisateur (comme appuyer sur un bouton) ou d'une entrée externe (telle qu'une réponse du réseau), l'UI doit être mise à jour pour refléter ces modifications. En réalité, l'UI est une représentation visuelle de l'état de l'application, tel qu'il est extrait de la couche de données.

Cependant, le format des données d'application obtenues à partir de la couche de données est différent de celui des informations à afficher. Par exemple, vous n'aurez peut-être besoin que d'une partie des données de l'interface utilisateur ou vous devrez fusionner deux sources de données différentes pour présenter des informations pertinentes à l'utilisateur. Quelle que soit la logique que vous appliquez, vous devez transmettre à l'interface utilisateur toutes les informations nécessaires à son affichage complet. La couche d'UI est le pipeline qui convertit les modifications de données d'application dans un formulaire que l'UI peut présenter, puis les affiche.

Dans une architecture classique, les éléments d'interface utilisateur de la couche d'UI dépendent des conteneurs d'état, qui à leur tour dépendent des classes de la couche de données ou de la couche de domaine facultative.
Figure 1. Rôle de la couche d'interface utilisateur dans l'architecture de l'application

Une étude de cas basique

Prenons l'exemple d'une application qui récupère les articles d'actualités pour qu'un utilisateur puisse les lire. L'application dispose d'un écran d'articles qui présente des articles disponibles en lecture et permet également aux utilisateurs connectés de placer des articles qui se démarquent vraiment. Étant donné qu'il peut y avoir de nombreux articles à un moment donné, le lecteur doit pouvoir parcourir les articles par catégorie. En résumé, l'application permet aux utilisateurs d'effectuer les opérations suivantes :

  • Afficher les articles disponibles en lecture
  • Parcourir les articles par catégorie
  • Se connecter et ajouter certains articles aux favoris
  • Accéder à certaines fonctionnalités premium, le cas échéant
Figure 2. Exemple d'application d'actualités pour une étude de cas sur l'interface utilisateur

Les sections suivantes utilisent cet exemple comme étude de cas pour présenter les principes du flux de données unidirectionnel, ainsi que pour illustrer les problèmes que ces principes permettent de résoudre dans le contexte de l'architecture de l'application pour la couche d'interface utilisateur.

Architecture de la couche d'interface utilisateur

Le termeUI fait référence aux éléments d'interface utilisateur tels que les activités et les fragments qui affichent les données, indépendamment des API qu'ils utilisent pour effectuer cette opération (vues ou Jetpack Compose). Étant donné que le rôle de la couche de données est de détenir, gérer et fournir l'accès aux données de l'application, la couche d'UI doit effectuer les étapes suivantes :

  1. Consommer les données d'application et les transformer en données que l'interface utilisateur peut facilement afficher.
  2. Consommer les données de l'interface utilisateur et les transformer en éléments d'interface utilisateur pour les présenter à l'utilisateur.
  3. Consommer les événements d'entrée utilisateur à partir de ces éléments d'UI assemblés et refléter leurs effets dans les données d'UI si nécessaire.
  4. Répéter les étapes 1 à 3 aussi longtemps que nécessaire.

Le reste de ce guide explique comment mettre en œuvre une couche d'interface utilisateur qui effectue ces étapes. Ce guide traite en particulier des tâches et des concepts suivants :

  • Comment définir l'état de l'interface utilisateur.
  • Flux de données unidirectionnel (UDF) comme moyen de produire et de gérer l'état de l'interface utilisateur.
  • Comment exposer l'état de l'interface utilisateur avec des types de données observables conformément aux principes de la fonction définie par l'utilisateur.
  • Implémenter une UI qui utilise l'état d'UI observable

Le point le plus fondamental concerne la définition de l'état de l'interface utilisateur.

Définir l'état de l'interface utilisateur

Reportez-vous à l'étude de cas décrite précédemment. En bref, l'interface utilisateur affiche une liste d'articles ainsi que des métadonnées pour chaque article. Ces informations présentées à l'utilisateur correspondent à l'état de l'interface utilisateur.

En d'autres termes, si l'interface utilisateur est visible par l'utilisateur, l'état de l'UI correspond à ce que l'application indique. Comme deux faces d'une même pièce, l'UI est une représentation visuelle de l'état de l'UI. Toute modification de l'état de l'interface utilisateur est immédiatement répercutée dans l'UI.

L'UI est le résultat d'une liaison entre des éléments d'interface utilisateur et un état d'interface utilisateur.
Figure 3. L'UI est le résultat d'éléments de liaison liés à l'état de l'interface utilisateur.

Reportez-vous à l'étude de cas. Afin de répondre aux exigences de l'application Actualités, les informations requises pour afficher entièrement l'interface utilisateur peuvent être encapsulées dans une classe de données NewsUiState définie comme suit :

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

Immuabilité

Dans l'exemple ci-dessus, la définition de l'état de l'interface utilisateur est immuable. L'avantage principal est que les objets immuables fournissent des garanties sur l'état de l'application à un instant T. L'interface utilisateur peut ainsi se concentrer sur un seul rôle : lire l'état et mettre à jour ses éléments en conséquence. Vous ne devez donc jamais modifier directement l'état de l'UI, sauf si l'UI elle-même est la seule source de ses données. Si vous ne respectez pas ce principe, alors il existera plusieurs sources de vérité pour une même information, ce qui entraînera des incohérences au niveau des données et des bugs.

Par exemple, si l'option bookmarked d'un objet NewsItemUiState de l'état de l'interface utilisateur de l'étude de cas a été mise à jour dans la classe Activity, elle entrerait en concurrence avec la couche de données en tant que source du statut d'un article ajouté aux favoris. Les classes de données immuables sont très utiles pour empêcher ce type d'antimodèle.

Conventions d'attribution de noms dans ce guide

Dans ce guide, les classes d'état de l'interface utilisateur sont nommées en fonction de la fonctionnalité ou de la partie de l'écran qu'elles décrivent. La convention est la suivante :

fonctionnalité + UiState.

Par exemple, l'état d'un écran d'actualités peut s'appeler NewsUiState, et celui d'un élément d'actualité dans une liste d'articles peut être NewsItemUiState.

Gérer l'état avec un flux de données unidirectionnel

Dans la section précédente, nous avons établi que l'état de l'interface utilisateur était un instantané immuable des détails nécessaires à l'affichage de l'interface utilisateur. Cependant, la nature dynamique des données dans les applications signifie que cet état peut changer au fil du temps. Cela peut être dû à une interaction de l'utilisateur ou à d'autres événements modifiant les données sous-jacentes utilisées pour renseigner l'application.

Ces interactions peuvent bénéficier d'un médiateur pour les traiter, en définissant la logique à appliquer à chaque événement et en effectuant les transformations requises sur les sources de données externes afin de créer un état de l'interface utilisateur. Ces interactions et leur logique peuvent être hébergées dans l'UI elle-même, mais cela peut rapidement devenir pénible lorsque l'UI commence à devenir plus que son nom l'exige : elle devient propriétaire, producteur, transformateur des données et plus encore. En outre, cela peut affecter la testabilité, car le code obtenu est un amalgame à couplage fort et sans limites discernables. En fin de compte, l'interface utilisateur pourrait bénéficier d'une réduction de la charge de travail. À moins que l'état de l'UI ne soit très simple, la seule responsabilité de l'UI doit être de consommer et d'afficher l'état de l'UI.

Cette section traite du flux de données unidirectionnel (UDF), un modèle d'architecture permettant d'appliquer cette séparation saine de responsabilités.

Conteneurs d'état

Les classes responsables de la production de l'état de l'interface utilisateur et contenant la logique nécessaire à cette tâche sont appelées conteneurs d'état. Il existe différentes tailles pour les conteneurs d'états, en fonction de la portée des éléments d'interface utilisateur correspondants qu'ils gèrent (que ce soit un widget unique, tel qu'une barre d'application inférieure, ou une destination de navigation).

Dans ce dernier cas, l'implémentation type est une instance d'un objet ViewModel. Toutefois, en fonction des exigences de l'application, une classe simple peut suffire. L'application d'actualités de l'étude de cas, par exemple, utilise une classe NewsViewModel en tant que conteneur d'état pour générer l'état de l'interface utilisateur de l'écran affiché dans cette section.

Il existe de nombreuses façons de modéliser la codépendance entre l'UI et son producteur d'état. Toutefois, étant donné que l'interaction entre l'interface utilisateur et sa classe ViewModel peut être largement assimilée à une entrée d'événement et à sa sortie d'état, la relation comme peut être illustrée comme dans le schéma suivant :

Les données d&#39;application circulent de la couche de données vers ViewModel. L&#39;état de l&#39;interface utilisateur est transmis du ViewModel vers les éléments de l&#39;UI, et les événements sont acheminés des éléments de l&#39;UI vers ViewModel.
Figure 4. Schéma illustrant le flux de données unidirectionnel (UDF) des fonctions définies par l'utilisateur dans l'architecture des applications.

Le modèle où l'état descend et les événements remontent est appelé flux de données unidirectionnel (UDF). Les conséquences de ce modèle pour l'architecture des applications sont les suivantes :

  • ViewModel contient et expose l'état à utiliser par l'UI. L'état de l'interface utilisateur correspond aux données d'application transformées par ViewModel.
  • L'UI informe le ViewModel des événements utilisateur.
  • ViewModel gère les actions de l'utilisateur et met à jour l'état.
  • L'état mis à jour est renvoyé à l'interface utilisateur pour être affiché.
  • Ce qui précède est répété pour tout événement qui provoque une mutation d'état.

Pour les destinations ou les écrans de navigation, le ViewModel fonctionne avec des dépôts ou des classes de cas d'utilisation pour obtenir des données et les transformer à l'état de l'interface utilisateur tout en incorporant les effets d'événements susceptibles de provoquer des mutations de l'état. L'étude de cas mentionnée précédemment contient une liste d'articles, ayant chacun un titre, une description, une source, le nom de l'auteur, la date de publication et si ces éléments ont été ajoutés aux favoris. L'interface utilisateur de chaque article se présente comme suit :

Figure 5. Interface utilisateur d'un article dans l'application de l'étude de cas.

Un utilisateur demandant à ajouter un article aux favoris est un exemple d'événement pouvant entraîner des mutations d'état. En tant que producteur d'état, il est de la responsabilité du ViewModel de définir toute la logique nécessaire pour remplir tous les champs dans l'état de l'UI et traiter les événements nécessaires à son affichage complet.

Un événement d&#39;interface utilisateur se produit lorsque l&#39;utilisateur ajoute un article à ses favoris. Le ViewModel informe la couche de données du changement d&#39;état. La couche de données applique la modification des données et met à jour les données de l&#39;application. Les nouvelles données d&#39;application comportant l&#39;article ajouté aux favoris sont transmises à ViewModel, qui génère ensuite le nouvel état de l&#39;interface utilisateur et le transmet aux éléments d&#39;UI pour affichage.
Figure 6. Schéma illustrant le cycle d'événements et de données dans l'UDF.

Les sections suivantes examinent de plus près les événements qui provoquent des changements d'état, ainsi que la manière dont ils peuvent être traités à l'aide de la fonction définie par l'utilisateur.

Types de logiques

L'ajout d'un article aux favoris est un exemple de logique métier, car il donne de la valeur à votre application. Pour en savoir plus à ce sujet, consultez la page sur la couche de données. Cependant, il existe plusieurs types de logiques qu'il est important de définir :

  • La logique métier est la mise en œuvre des exigences produit pour les données d'application. Comme indiqué précédemment, vous pouvez ajouter un article à vos favoris dans l'application de l'étude de cas. La logique métier est généralement placée dans les couches de domaine ou de données, mais jamais dans la couche d'interface utilisateur.
  • La logique de comportement de l'interface utilisateur ou la logique d'interface utilisateur indique comment afficher les changements d'état à l'écran. Par exemple, vous pouvez obtenir le texte à afficher à l'écran avec Resources sur Android, naviguer vers un écran spécifique lorsque l'utilisateur clique sur un bouton ou afficher un message utilisateur à l'aide d'un toast ou d'un snackbar.

La logique de l'interface utilisateur, en particulier lorsqu'elle implique des types d'UI tels que Context, doit résider dans l'interface utilisateur, et non dans le ViewModel. Si l'UI devient plus complexe et que vous souhaitez déléguer la logique de l'UI à une autre classe pour favoriser la testabilité et la séparation des préoccupations, vous pouvez créer une classe simple en tant que conteneur d'état. Les classes simples créées dans l'interface utilisateur peuvent prendre des dépendances du SDK Android, car elles suivent le cycle de vie de l'interface utilisateur. Les objets ViewModel ont une durée de vie plus longue.

Pour en savoir plus sur les conteneurs d'états et la manière dont ils s'inscrivent dans le contexte d'aide à la création de l'interface utilisateur, consultez le guide Jetpack Compose State.

Pourquoi utiliser l'UDF ?

L'UDF modélise le cycle de production de l'état, comme illustré dans la Figure 4. Il sépare également le lieu d'origine des changements d'état, le lieu où ils sont transformés et le lieu où ils sont enfin utilisés. Cette séparation permet à l'interface utilisateur de faire exactement ce que son nom implique : afficher des informations en observant les changements d'état et transmettre l'intent de l'utilisateur en communiquant ces modifications à ViewModel.

En d'autres termes, la fonction définie par l'utilisateur autorise les éléments suivants :

  • Cohérence des données. Il n'existe qu'une source d'informations unique pour l'interface utilisateur.
  • Testabilité. La source d'état est isolée et donc testable, indépendante de l'UI.
  • Facilité de gestion. La mutation d'un état suit un modèle bien défini où les mutations résultent à la fois des événements utilisateur et des sources de données à partir desquelles ils extraient.

Exposer l'état de l'interface utilisateur

Après avoir défini l'état de l'interface utilisateur et déterminé la manière dont vous allez gérer la production de cet état, l'étape suivante consiste à présenter l'état produit à l'interface utilisateur. Étant donné que vous utilisez la fonction définie par l'utilisateur pour gérer la production de l'état, vous pouvez considérer l'état produit comme un flux. En d'autres termes, plusieurs versions de l'état seront produites au fil du temps. Par conséquent, vous devez exposer l'état de l'interface utilisateur dans un conteneur de données observable, tel que LiveData ou StateFlow. En effet, l'UI peut ainsi réagir à toute modification d'état sans avoir à extraire manuellement les données directement à partir de ViewModel. Ces types ont également l'avantage de toujours mettre en cache la dernière version de l'état de l'interface utilisateur, ce qui est utile pour restaurer rapidement l'état après une modification de la configuration.

Vues

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = …
}

Compose

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = …
}

Pour en savoir plus sur LiveData en tant que conteneur de données observable, consultez cet atelier de programmation. Pour une présentation similaire des flux Kotlin, consultez la section Flux Kotlin sur Android.

Dans les cas où les données exposées à l'interface utilisateur sont relativement simples, il est souvent utile de les encapsuler dans un type d'état de l'interface utilisateur, car elles révèlent la relation entre les émissions du conteneur d'état et l'écran ou l'élément d'interface utilisateur qui lui est associé. De plus, à mesure que l'élément d'interface utilisateur se complexifie, il est toujours plus facile d'ajouter à la définition de l'état de l'interface utilisateur pour prendre en compte les informations supplémentaires nécessaires à l'affichage de l'élément.

Une façon courante de créer un flux UiState consiste à exposer un flux modifiable secondaire en tant que flux immuable à partir de ViewModel, par exemple en exposant MutableStateFlow<UiState> en tant que StateFlow<UiState>.

Vues

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Compose

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

ViewModel peut alors exposer des méthodes qui modifient en interne l'état en publiant les mises à jour que l'UI doit utiliser. Prenons, par exemple, le cas où une action asynchrone doit être effectuée. Une coroutine peut être lancée à l'aide de viewModelScope, et l'état modifiable peut être mis à jour une fois l'opération terminée.

Vues

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

Compose

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

   var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

Dans l'exemple ci-dessus, la classe NewsViewModel tente de récupérer des articles pour une certaine catégorie, puis reflète le résultat de la tentative, qu'il s'agisse d'une réussite ou d'un échec, dans l'état de l'UI permettant à celle-ci de réagir de manière appropriée. Consultez la section Afficher les erreurs à l'écran pour en savoir plus sur la gestion des erreurs.

Facteurs supplémentaires

En plus des conseils précédents, tenez compte des points suivants lorsque vous exposez l'état de l'interface utilisateur :

  • Un objet d'état de l'interface utilisateur doit gérer les états associés les uns aux autres. Cela permet de réduire les incohérences et de rendre le code plus facile à comprendre. Si vous affichez la liste des articles d'actualités et le nombre de favoris dans deux flux différents, vous pouvez vous retrouver dans une situation où l'un a été mis à jour et l'autre non. Lorsque vous utilisez un seul flux, les deux éléments sont maintenus à jour. En outre, certaines logiques métier peuvent nécessiter une combinaison de sources. Par exemple, vous pouvez n'afficher un bouton de favori que si l'utilisateur est connecté et qu'il est abonné à un service d'actualités premium. Vous pouvez définir une classe d'état de l'interface utilisateur comme suit :

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    Dans cette déclaration, la visibilité du bouton de favori est une propriété dérivée de deux autres propriétés. À mesure que la logique métier se complexifie, il est de plus en plus important d'avoir une classe UiState unique où toutes les propriétés sont immédiatement disponibles.

  • États de l'UI : un ou plusieurs flux ? Le principe directeur suivant pour choisir entre l'exposition de l'état de l'interface utilisateur dans un seul flux ou dans plusieurs flux est la puce précédente : la relation entre les éléments émis. Le principal avantage d'une exposition à un seul flux est la commodité et la cohérence des données : les consommateurs d'état disposent toujours des informations les plus récentes à tout moment. Toutefois, dans certains cas, des flux d'état distincts de ViewModel peuvent s'avérer appropriés :

    • Types de données non liés : certains états nécessaires à l'affichage de l'interface utilisateur peuvent être complètement indépendants les uns des autres. Dans de tels cas, les coûts de l'association de ces états disparates peuvent dépasser les avantages, en particulier si l'un de ces états est mis à jour plus fréquemment que l'autre.

    • Différence UiState : plus il y a de champs dans un objet UiState, plus il est probable que le flux émette en raison de l'un de ses champs en cours de mise à jour. Comme les vues n'ont pas de mécanisme différent pour déterminer si les émissions consécutives sont différentes ou identiques, chaque émission entraîne une mise à jour de la vue. Cela signifie qu'une atténuation à l'aide des API ou de méthodes Flow telles que distinctUntilChanged() sur le LiveData peut être nécessaire.

Utiliser l'état de l'interface utilisateur

Pour utiliser le flux d'objets UiState dans l'interface utilisateur, utilisez l'opérateur terminal pour le type de données observables que vous utilisez. Par exemple, pour LiveData, utilisez la méthode observe(), et pour les flux Kotlin, utilisez la méthode collect() ou ses variantes.

Lorsque vous utilisez des conteneurs de données observables dans l'UI, assurez-vous de prendre en compte le cycle de vie de l'interface utilisateur. C'est important, car l'UI ne doit pas observer l'état de l'UI lorsque la vue n'est pas présentée à l'utilisateur. Pour en savoir plus à ce sujet, consultez cet article de blog. Lorsque vous utilisez LiveData, LifecycleOwner se charge implicitement des problèmes de cycle de vie. Lorsque vous utilisez des flux, il est préférable de gérer cela avec le champ d'application de coroutine approprié et l'API repeatOnLifecycle :

Vues

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Afficher les opérations en cours

Un moyen simple de représenter les états de chargement dans une classe UiState consiste à utiliser un champ booléen :

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

La valeur de cette option représente la présence ou l'absence d'une barre de progression dans l'interface utilisateur.

Vues

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Compose

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Afficher les erreurs à l'écran

L'affichage des erreurs dans l'interface utilisateur est semblable à celui des opérations en cours, car elles sont toutes deux facilement représentées par des valeurs booléennes indiquant leur présence ou leur absence. Toutefois, les erreurs peuvent également inclure un message associé permettant de relayer l'erreur à l'utilisateur, ou une action associée à l'utilisateur qui retente l'opération ayant échoué. Par conséquent, lorsqu'une opération en cours est en phase de chargement ou non, il peut être nécessaire de modéliser les états d'erreur avec des classes de données qui hébergent les métadonnées appropriées pour le contexte de l'erreur.

Prenons l'exemple de la section précédente, qui comportait une barre de progression lors de la récupération des articles. Si cette opération entraîne une erreur, vous pouvez afficher un ou plusieurs messages répertoriant les erreurs.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

Les messages d'erreur peuvent ensuite être présentés à l'utilisateur sous la forme d'éléments d'interface utilisateur tels que des snackbars. Étant donné que ces événements sont liés à la production et à la consommation des événements d'UI, consultez la page Événements d'UI pour en savoir plus.

Exécutions de threads et simultanéité

Toute tâche effectuée dans un ViewModel doit être sécurisée, car elle peut être appelée à partir du thread principal. En effet, les couches de données et de domaine sont chargées de déplacer le travail vers un autre thread.

Si un ViewModel effectue des opérations de longue durée, il est également responsable de déplacer cette logique vers un thread d'arrière-plan. Les coroutines Kotlin sont un excellent moyen de gérer les opérations simultanées, et les composants d'architecture Jetpack leur sont directement intégrés. Pour en savoir plus sur l'utilisation de coroutines dans les applications Android, consultez Coroutines Kotlin sur Android.

Les changements de navigation dans les applications sont souvent dus à des émissions qui ressemblent à celles d'un événement. Par exemple, après la connexion d'une classe SignInViewModel, le champ UiState peut avoir un champ isSignedIn défini sur true. Les déclencheurs de ce type doivent être utilisés comme ceux décrits dans la section Utiliser l'état de l'interface utilisateur ci-dessus, à la différence près que la mise en œuvre de la consommation doit porter sur le composant de navigation.

Paging

La bibliothèque Paging est utilisée dans l'interface utilisateur avec un type appelé PagingData. Étant donné que PagingData représente et contient des éléments qui peuvent changer au fil du temps (en d'autres termes, il ne s'agit pas d'un type immuable), il ne doit pas être représenté dans un état d'interface utilisateur immuable. Vous devez l'exposer à partir du ViewModel indépendamment dans son propre flux. Consultez l'atelier de programmation Android Paging pour obtenir un exemple spécifique.

Animations

Pour assurer des transitions de navigation fluides de premier niveau, vous pouvez attendre que le second écran charge des données avant de lancer l'animation. Le framework de vue Android fournit des hooks pour retarder les transitions entre les destinations de fragment avec les API postponeEnterTransition() et startPostponedEnterTransition(). Ces API permettent de s'assurer que les éléments de l'interface utilisateur du deuxième écran (généralement une image extraite du réseau) sont prêts à être affichés avant l'animation de la transition vers cet écran. Pour en savoir plus et connaître les détails de la mise en œuvre, consultez l'exemple Android Motion.

Exemples

Les exemples Google suivants illustrent l'utilisation de la couche de l'interface utilisateur. Parcourez-les pour voir ces conseils en pratique :