Couches architecturales de Jetpack Compose

Cette page offre une vue d'ensemble des couches architecturales qui composent Jetpack Compose, ainsi que les principes fondamentaux sur lesquels repose cette architecture.

Jetpack Compose n'est pas un projet monolithique unique. Il se compose de plusieurs modules assemblés pour former un ensemble. Comprendre les différents modules de Jetpack Compose vous permet :

  • d'utiliser le niveau d'abstraction approprié pour créer votre application ou votre bibliothèque ;
  • de déterminer à quel moment vous pouvez "passer à un niveau inférieur" pour plus de contrôle ou de personnalisation ;
  • de minimiser vos dépendances.

Couches

Les principales couches de Jetpack Compose sont les suivantes :

Figure 1 : Principales couches de Jetpack Compose

Chaque couche repose sur les niveaux inférieurs et combine des fonctionnalités pour créer des composants de niveau supérieur. Chaque couche repose sur les API publiques des couches inférieures pour vérifier les limites du module et vous permettre de remplacer n'importe quelle couche si nécessaire. Examinons ces couches de bas en haut.

Runtime (durée d'exécution)
Ce module présente les principes de base de l'environnement d'exécution Compose, tels que remember, mutableStateOf, l'annotation @Composable et SideEffect. Vous pourriez envisager de vous appuyer directement sur cette couche si vous n'avez besoin que des fonctionnalités de gestion des arborescences de Compose, et non de son interface utilisateur.
UI (interface utilisateur)
La couche de l'interface utilisateur est composée de plusieurs modules (ui-text, ui-graphics, ui-tooling, etc.). Ces modules implémentent les principes de base du kit d'interface utilisateur, tels que LayoutNode, Modifier, les gestionnaires d'entrée, les mises en page personnalisées et le dessin. Vous pourriez envisager de vous appuyer sur cette couche si vous n'avez besoin que des concepts fondamentaux d'un kit d'interface utilisateur.
Foundation (fondation)
Ce module fournit des composants qui ne dépendent pas du système de conception pour l'interface utilisateur de Compose, tels que Row et Column, LazyColumn, la reconnaissance de gestes spécifiques, etc. Vous pourriez envisager de vous appuyer sur la couche de fondation pour créer votre propre système de conception.
Material
Ce module permet d'implémenter le système Material Design pour l'interface utilisateur de Compose. Il fournit un système de thématisation, des composants stylisés, des indications d'ondes et des icônes. Appuyez-vous sur cette couche lorsque vous utilisez Material Design dans votre application.

Principes de conception

L'un des principes directeurs de Jetpack Compose est de fournir de petites fonctionnalités spécifiques qui peuvent être assemblées (ou composées) plutôt que quelques composants monolithiques. Cette approche présente plusieurs avantages.

Contrôle

Les composants de niveau supérieur ont tendance à en faire plus, mais limitent votre niveau de contrôle direct. Si vous avez besoin de plus de contrôle, vous pouvez "passer à un niveau inférieur" pour utiliser un composant d'un niveau plus bas.

Par exemple, si vous souhaitez animer la couleur d'un composant, vous pourriez utiliser l'API animateColorAsState :

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

Si vous souhaitez que ce composant soit grisé au démarrage, vous ne pouvez pas le faire avec cette API. À la place, vous pouvez passer au niveau inférieur pour utiliser l'API Animatable:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

L'API animateColorAsState de niveau supérieur est elle-même basée sur l'API Animatable de niveau inférieur. L'utilisation de l'API de niveau inférieur est plus complexe, mais offre plus de contrôle. Choisissez le niveau d'abstraction qui correspond le mieux à vos besoins.

Personnalisation

L'assemblage de composants de niveau supérieur à partir de composants inférieurs facilite grandement la personnalisation des composants, si besoin. Prenons l'exemple de l'implémentation de Button (bouton) fournie par la couche Material :

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Un Button est assemblé à partir de quatre éléments :

  1. Un Surface Material qui fournit l'arrière-plan, la forme, la gestion des clics, etc.

  2. Un CompositionLocalProvider qui modifie la valeur alpha du contenu lorsque le bouton est activé ou désactivé

  3. Un ProvideTextStyle qui définit le style de texte par défaut à utiliser

  4. Un Row qui fournit la règle de mise en page par défaut pour le contenu du bouton.

Nous avons omis certains paramètres et commentaires pour clarifier la structure, mais l'ensemble du composant ne comporte qu'une quarantaine de lignes de code, car il assemble simplement ces quatre composants pour implémenter le bouton. Les composants tels que Button sont définis par rapport aux paramètres qu'ils exposent, ce qui permet de se limiter aux personnalisations courantes plutôt que de multiplier les paramètres susceptibles de rendre plus difficile l'utilisation d'un composant. Les composants Material, par exemple, proposent des personnalisations spécifiées dans le système Material Design, ce qui permet de suivre facilement les principes de Material Design.

Toutefois, si vous souhaitez effectuer une personnalisation au-delà des paramètres d'un composant, vous pouvez "passer au niveau inférieur" et dupliquer un composant. Par exemple, Material Design indique que l'arrière-plan des boutons doit être uni. Si vous avez besoin d'un arrière-plan dégradé, cette option n'est pas compatible avec les paramètres Button. Dans ce cas, vous pouvez utiliser l'implémentation Material de Button comme référence et créer votre propre composant :

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

L'implémentation ci-dessus continue à utiliser les composants de la couche Material, tels que les concepts de la valeur alpha du contenu actuel et le style de texte actuel. Cependant, elle remplace la Surface Material par une Row et le stylise pour obtenir l'apparence souhaitée.

Si vous ne souhaitez pas du tout utiliser de concepts de Material, par exemple si vous créez votre propre système de conception sur mesure, vous pouvez choisir de n'utiliser que des composants de la couche de fondation:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose réserve les noms les plus simples aux composants de niveau supérieur. Par exemple, androidx.compose.material.Text est basé sur androidx.compose.foundation.text.BasicText. Cela vous permet de mettre en place votre propre implémentation avec le nom le plus visible si vous souhaitez remplacer les niveaux supérieurs.

Choisir la bonne abstraction

La philosophie de Compose, qui consiste à créer des composants par couche et réutilisables, implique de ne pas toujours chercher les composants fondamentaux de niveau inférieur. De nombreux composants de niveau supérieur offrent non seulement plus de fonctionnalités, mais implémentent souvent de bonnes pratiques telles que la prise en charge de l'accessibilité.

Par exemple, si vous souhaitez ajouter la prise en charge des gestes à votre composant personnalisé, vous pouvez partir de zéro en utilisant Modifier.pointerInput, mais il existe d'autres composants de niveau supérieur, construits sur cette base et pouvant offrir un meilleur point de départ, tels que Modifier.draggable, Modifier.scrollable. ou Modifier.swipeable.

En règle générale, privilégiez le composant de niveau supérieur offrant la fonctionnalité dont vous avez besoin pour bénéficier des bonnes pratiques qu'il comprend.

En savoir plus

Pour découvrir un exemple de création d'un système de conception personnalisé, consultez l'exemple Jetsnack.