Structurer votre interface utilisateur Compose

Dans Compose, l'UI est non modifiable. Il n'est pas possible de la mettre à jour après sa conception. Vous pouvez contrôler l'état de votre interface utilisateur. À chaque modification de l'interface utilisateur, Compose recrée les parties de l'arborescence qui ont été modifiées. Les composables peuvent accepter des événements d'état et d'exposition. Par exemple, un objet TextField accepte une valeur et expose un rappel onValueChange qui demande au gestionnaire de rappel de modifier la valeur.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

Étant donné que les composables acceptent des événements "state" et "expose", le modèle de flux de données unidirectionnel s'adapte bien à Jetpack Compose. Ce guide explique comment implémenter le modèle de flux de données unidirectionnel, implémenter des événements et des conteneurs d'état, et utiliser les ViewModels dans Compose.

Flux de données unidirectionnel

Un flux de données unidirectionnel (UDF) est un modèle de conception dans lequel l'état redescend et les événements remontent. En suivant le flux de données unidirectionnel, vous pouvez dissocier les composables qui affichent l'état dans l'interface utilisateur des parties de votre application qui stockent et modifient l'état.

La boucle de mise à jour de l'UI pour une application utilisant un flux de données unidirectionnel se présente comme suit :

  • Événement : une partie de l'interface utilisateur génère un événement et le fait remonter (p. ex. un clic sur le bouton transmis au ViewModel qui va le gérer), ou un événement transmis à partir d'autres couches de votre application (p. ex. un message qui indique l'expiration de la session utilisateur).
  • Mettre à jour l'état : un gestionnaire d'événements peut modifier l'état.
  • État de l'affichage : le conteneur d'état transmet l'état, et l'interface utilisateur l'affiche.

Figure 1. Flux de données unidirectionnel

Ce mode d'utilisation de Jetpack Compose offre plusieurs avantages :

  • Facilité des tests : en dissociant l'état de l'UI qui l'affiche, il est plus facile de tester l'état et l'UI de façon isolée.
  • Encapsulation de l'état : comme l'état peut uniquement être actualité à un seul endroit et qu'il n'existe qu'une seule référence fiable pour l'état d'un composable, il y aura moins de risque d'introduire un bug créé par des états contradictoires.
  • Cohérence de l'UI : toutes les modifications d'état sont immédiatement reflétées dans l'UI grâce à l'utilisation de conteneurs d'état observables, comme StateFlow ou LiveData.

Flux de données unidirectionnel dans Jetpack Compose

Le fonctionnement des composables repose sur un état et des événements. Par exemple, un TextField n'est actualisé que lorsque son paramètre value est mis à jour et qu'il expose un rappel onValueChange, un événement qui demande la modification de la valeur. Compose définit l'objet State comme un conteneur de valeurs, et toute modification apportée à la valeur d'état déclenche une recomposition. Vous pouvez conserver l'état dans un remember { mutableStateOf(value) } ou un rememberSaveable { mutableStateOf(value) selon la durée de conservation de la valeur.

Le type de la valeur du composable TextField est String. Elle peut donc provenir de n'importe quelle source : d'une valeur codée en dur, d'un ViewModel ou transmise à partir du composable parent. Vous n'avez pas besoin de le conserver dans un objet State, mais vous devez mettre à jour la valeur lorsque onValueChange est appelé.

Définir les paramètres du composable

Lorsque vous définissez les paramètres d'état d'un composable, tenez compte des questions suivantes :

  • Le composable est-il réutilisable ou flexible ?
  • Comment les paramètres d'état affectent-ils les performances de ce composable ?

Pour encourager leur dissociation et leur réutilisation, chaque composable doit contenir le moins d'informations possible. Par exemple, lorsque vous créez un composable qui contient l'en-tête d'un article d'actualité, ne transmettez que les informations à afficher, plutôt que l'article entier :

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

L'utilisation de paramètres individuels améliore parfois les performances. Par exemple, si News contient plus d'informations que title et subtitle, chaque fois qu'une nouvelle instance de News est transmise Header(news), le composable se recompose, même si title et subtitle n'ont pas changé.

Réfléchissez bien au nombre de paramètres que vous transmettez. L'excès de paramètres dans une fonction réduit son ergonomie. Dans ce cas, il est préférable de les regrouper dans une classe.

Les événements dans Compose

Chaque entrée de votre application doit être représentée comme un événement : les appuis, les modifications de texte, et même les minuteurs ou autres mises à jour. Étant donné que ces événements modifient l'état de votre UI, c'est ViewModel qui doit les gérer et mettre à jour l'état de l'UI.

La couche de l'UI ne doit jamais changer d'état en dehors d'un gestionnaire d'événements, car cela peut entraîner des incohérences et des bugs dans votre application.

Préférez les valeurs immuables pour les lambdas d'état et de gestionnaire d'événements. Cette approche présente les avantages suivants :

  • Vous améliorez la réutilisation.
  • Vous vous assurez que votre UI ne modifie pas directement la valeur de l'état.
  • Vous éviterez les problèmes de simultanéité en vous assurant que l'état n'est pas modifié à partir d'un autre thread.
  • Bien souvent, cela permet de réduire la complexité du code.

Par exemple, un composable qui accepte un String et un lambda comme paramètres peut être appelé à partir de nombreux contextes et est hautement réutilisable. Supposons que la barre d'application supérieure de votre application affiche toujours du texte et dispose d'un bouton "Retour". Vous pouvez définir un composable MyAppTopAppBar plus générique qui reçoit le texte et le bouton "Retour" en tant que paramètres :

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModels, états et événements : exemple

En utilisant ViewModel et mutableStateOf, vous pouvez également introduire un flux de données unidirectionnel dans votre application si l'une des conditions suivantes est remplie :

  • L'état de votre interface utilisateur est exposé via des conteneurs d'état observables, tels que StateFlow ou LiveData.
  • ViewModel gère les événements provenant de l'interface utilisateur ou d'autres couches de votre application, et met à jour le conteneur d'état en fonction des événements.

Par exemple, lorsque vous implémentez un écran de connexion, le fait d'appuyer sur un bouton Se connecter doit entraîner l'affichage d'un élément de chargement et d'un appel réseau. Si la connexion aboutit, l'application accède à un autre écran. En cas d'erreur, l'application affiche une snackbar. Pour modéliser l'état de l'écran et l'événement, procédez comme suit :

L'écran présente quatre états :

  • Déconnecté : lorsque l'utilisateur ne s'est pas encore connecté.
  • En cours : lorsque votre application tente actuellement de connecter l'utilisateur en effectuant un appel réseau.
  • Erreur : une erreur s'est produite lors de la connexion.
  • Connecté : l'utilisateur est connecté.

Vous pouvez modéliser ces états sous la forme d'une classe scellée. ViewModel expose l'état en tant que State, définit l'état initial et le met à jour si nécessaire. ViewModel gère également l'événement de connexion en exposant une méthode onSignIn().

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

En plus de l'API mutableStateOf, Compose fournit des extensions pour enregistrer LiveData, Flow et Observable en tant qu'écouteur et représenter la valeur sous forme d'état.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

En savoir plus

Pour en savoir plus sur l'architecture dans Jetpack Compose, consultez les ressources suivantes :

Exemples