Initier son design system Android avec Jetpack Compose

Je crée une application Android pour faciliter l’apprentissage de l’albanais. Je ne suis pas designer mais développeur. Je travaille seul dessus. Et malheureusement, je n’ai pas de réelles compétences pour créer des maquettes de mon application.

Au travers d'échanges sur des communautés Slack, j’ai découvert UXPilot. L’idée est simple : générer des maquettes via IA à partir de prompts. Suite à cela, j’ai voulu m’exercer à créer un Design System mobile.

Qu’est-ce qu’un Design System ?

Selon Figma

💡
Fondamentalement, un design system est un ensemble de blocs de construction et de standards qui aident à garder l'apparence et l'expérience des produits cohérentes. Voyez-le comme un modèle qui offre un langage unifié et un cadre de travail structuré qui guident les équipes à travers le processus complexe de création de produits numériques. Un design system peut aider à réduire la quantité de temps dépensée à recréer des éléments et des patrons tout en concevant et construisant des produits et interfaces à l'échelle.

Souvent, on a l’intuition de vouloir réutiliser des composants lorsqu’on développe une application. En tant que développeur, on travaille la plupart du temps avec une maquette. Elle spécifie des composants, des couleurs, des formes ou encore des ombres. Recréer chaque écran de l’application de zéro est au mieux chronophage, au pire générateur d’erreurs. 

Pour illustrer mon propos, je vous propose de suivre l’exemple du template Figma Coinpay Fintech Finance Mobile App UI kit (Community) par YouthMind.

exemple de template figma

En observant le parcours d’onboarding, on voit plusieurs choses :

  • La couleur de fond est la même 
  • Les boutons sont similaires
  • La police d’écriture est aussi partagée
  • Les espacements sont les mêmes
  • …vous voyez le principe

Au départ, lorsqu’on veut réutiliser, on va souvent commencer par faire un UI Kit, ou une component library. C'est-à-dire rassembler les composants réutilisés dans un module à part, et ensuite s’en servir là où on en a besoin.

Le design system va plus loin dans la mesure où il fournit aussi des conventions et des pratiques.

À la recherche d’exemples de Design System Mobile

Si les exemples de produits et de design system orientés web sont pléthoriques, c’est un peu moins vrai pour le mobile.

Une des références, peu importe la plateforme, est Vitamin, le design system de decathlon. Ce qui est génial, c’est que toutes les ressources sont accessibles :

  • Le Zeroheight, qui sert à inscrire les guidelines et centraliser les informations du Design System
  • L’implémentation Android XML
  • L’implémentation Jetpack Compose
  • Le Figma
  • Et les équivalences pour toutes les autres technologies

Zeroheight

ZeroHeight est un produit SaaS qui aide à créer son propre design system. Cela se présente sous forme d’un référentiel web :

design system vitamin

Sur la gauche, on retrouve plusieurs catégories clés dont :

  • L’onboarding (get started)
  • Les guidelines. C’est ici qu’on retrouve toutes les règles liées aux composants et aux plateformes.
  • Les design tokens (on en rediscutera plus loin).
  • Les composants

La gestion des breakpoints et form factors (catégories d’écran sur Android) est une de ces guidelines :

gestion des breakpoints dans vitamin

Statut de Vitamin

Malheureusement, ces versions de Vitamin ne sont plus activement maintenues depuis 2 ans. Decathlon explique pourquoi ici. Malgré cela, ces ressources sont précieuses pour démarrer son design system tant elles sont complètes et industrialisées. Rappelons qu'elles étaient utilisées par plusieurs centaines de développeurs et sur plusieurs dizaines d’applications mobiles.

Autres ressources

Si je connaissais Vitamin au préalable au travers de conférences et de mon réseau, je ne connaissais pas d’autres ressources orientées mobiles. Design Systems Repo et The Component Gallery sont justement des sites qui en référencent. Ce que j’apprécie avec The Component Gallery, c’est qu’il y a des filtres par technologie :

nombre de design systems mobiles dans the component gallery

Comme je le disais plus haut, l’écrasante majorité des ressources sont pour le web. Ici, seulement 5 projets sont identifiés comme mobile :

design systems mobiles dans the component gallery

Parmi ces résultats, il n’y a que deux projets qui sont réalisés par des entreprises autres que les big tech et qui ont un GitHub pour analyser leur implémentation : visa et backpack (Skyscanner).

Visa Product Design System 

L'organisation de ce design system est moins granulaire que celle de Vitamin. Il y a un menu pour les designers et un autre pour les développeurs. On retrouve sinon sur la gauche les mêmes éléments que sur Vitamin, mais avec une hiérarchie plus horizontale. Visa a aussi choisi de développer sa propre plateforme. Ici, pas de Zeroheight.

design system visa

En regardant le composant Button, on observe que Visa a choisi Flutter pour sa version mobile : 

composant bouton dans le design system

Un aperçu non cliquable du rendu est présent, ainsi que des exemples d’usage via le code. Les variantes de couleurs sont aussi spécifiées sur cette page.

Sur GitHub, le projet semble plutôt activement maintenu, le dernier commit datant du mois dernier. On y apprend d’ailleurs que ce design system s’appelle Nova.

Puisque je développe une application seulement Android, je ne me suis pas appuyé sur l’implémentation de Nova.

Backpack (Skyscanner)

Backpack se présente aussi différemment des deux premiers. C’est également une plateforme développée en interne : 

design system backpack de Skyscanner

Cette fois-ci, il y a bien des spécifications en Android Natif : 

Boutons dans Backpack

Contrairement à Vitamin, il y a un seul répertoire sur GitHub pour les versions XML et Compose du design system.

Difficultés sur les Design Systems mobiles

exemple de problématiques d'alignement entre design system web et mobile

On voit que faire coexister web et mobile n’est pas évident. Les entreprises ayant souvent plus de designers web que mobiles (parce qu’il y a souvent plus de produits web), cela se ressent aussi souvent dans les design systems. Les guidelines transverses ont tendance à être déclinées uniquement sur leur version web. Ci-dessus, les best practices pour un avatar sur Nova abordent l’accessibilité des icônes et préconisent l’utilisation de labels qui n’existent pas sur mobile.

Quid de l’automatisation du testing et des exemples de code ?

Là où en web nous sommes plutôt chanceux avec des solutions comme Storybook, en mobile nous n’avons pas d’équivalent simple. C'est-à-dire un outil qui concentre à la fois les composants du design system, leur implémentation et leur rendu. Dans les exemples étudiés, le code est souvent à côté de captures d’écrans d’une application de démonstration.

exemple de storybook

source: storybook

Storybook, c’est facile à intégrer et à utiliser. Avec leur autre produit Chromatic, la non régression visuelle des composants est automatisable rapidement.

Une extension native existe mais n’est plus maintenue. Elle se base sur Appetize.io.

Conclusion sur l’étude de marché

Finalement, même si Decathlon choisit une solution sur étagère alors que Visa et Skyscanner ont développé la leur, les approches restent analogues :

  • Proposer un service web qui fournit de la documentation sur le Design System
  • Donner un nom à son Design System. C’est un produit à part entière
  • Créer des bibliothèques techniques qui implémentent les guidelines proposées
  • OpenSourcer le tout

Bien sûr, il s’agit du cas idéal. D’autres entreprises voudront rendre le tout privé. Cependant, opensourcer permet à la fois de travailler son image de marque et de profiter de réductions par exemple en termes de CI/CD. De nombreux services comme GitHub sont gratuits ou à tarifs réduits pour les produits opensource.

Implémentation du Design System

J’ai choisi de surtout me baser sur Backpack. Sa bibliothèque Jetpack Compose est activement maintenue contrairement à celle de Vitamin.

Design tokens

Pour démarrer mon Design System, j’ai implémenté les Design Tokens. Je me suis référé à un excellent article de Contentful pour cela. 

Voici leur définition des Design Tokens (traduite de l’anglais) : 

Les Design Tokens sont vitaux pour capturer toutes les décisions de conceptions de votre design system. Ces décisions couvrent une variété d'éléments qui définissent votre produit et votre marque, comme les couleurs, les typographies, les bordures ou encore les animations.

Les Design Tokens se répartissent en trois catégories : primitifs, sémantiques et composants.

Primitive tokens

Ce sont les tokens les plus simples, ils n’ont pas vraiment de sens au sein de votre produit. Par exemple, un token appelé color-white correspondant à du blanc sera un token primitif. On les retrouve souvent classés dans des packages Foundation ou Core. 

On pourra retrouver ces types de primitive tokens :

Nom

Description

Exemple

color

Une couleur

color-white -> FFFFFFF

blur

Le niveau de flou

blur-none -> Aucun flou

animation 

La durée d’une animation

animation-xs -> 200ms

shadow / elevation

Les ombres du composant

elevation-sm -> 0dp

spacing

Les espacements (margin, padding)

spacing-xs -> 1dp

ripple / ripple alpha

L’effet de sélection d’un bouton

ripple-alpha-xs -> RippleAlpha(

        draggedAlpha = 0.1f,

        focusedAlpha = 0.1f,

        hoveredAlpha = 0.1f,

        pressedAlpha = 0.1f,

)

border

La taille des bordures de composants

border-xs -> 1dp

radius

Le radius (le niveau d’arrondi) appliqué aux composants

radius-xs -> 1dp

line height

La hauteur de ligne 

line-height-xs -> 16sp

font size

La taille de la police d’écriture

font-size-xs -> 12sp

letter spacing

L’écart entre les caractères d’une chaîne de caractères

letter-spacing-xs -> -(0.02).em

Semantic tokens

Comme leur nom l’indique, ces tokens sont porteurs de sens. Là où un token primitif pris seul pourrait être réutilisé dans n’importe quel contexte, le token sémantique est spécifique à son utilisation.

exemple de semantic tokens

Source : https://www.contentful.com/blog/design-token-system 

C’est avec les tokens sémantiques que l’on définit les tokens utilisés par défaut (exemple : text-default), dans une brand particulière (exemple : thème light ou thème dark), ou encore avec une interaction spécifique (hover, active, etc.).

catégories de semantic tokens

Source : https://www.contentful.com/blog/design-token-system 

Au final, la composition de ces catégories aide à la définition de ces couleurs :

composition d'un nom pour un semantic token

La composition des couleurs pour les bordures se lisent comme ceci :

  • border-default-hover
  • border-primary-hover
  • etc.

Component tokens

C’est un nouveau niveau d’abstraction. Les tokens de composants s’appuient sur les tokens sémantiques. L’intérêt est de pouvoir personnaliser le rendu par composant sans casser toute la structure de tokens primitifs et sémantiques.

exemples de component tokens

Source : https://www.contentful.com/blog/design-token-system 

Ici, le bouton se décline en deux marques. Ils ont un rendu très différent. button-radius est un token de composant spécifique au bouton. Ainsi, chaque marque peut personnaliser la taille du radius. Vous aurez aussi noté que la couleur n’est pas la même mais aucun button-bg n’a été créé. Cela met en lumière que le bouton devrait utiliser la couleur bg-primary de la marque et ne devrait pas être remplacée par une autre. C’est une affaire de choix et de possibilités que vous offrez aux utilisateurs de votre design system.

Implémentation

Structure des dossiers

J’ai décidé dès le départ de créer un module séparé qui contient l’implémentation de mon design system. Je l'ai appelé Tosk, en référence au dialecte qui correspond aujourd’hui à l’albanais officiel.

On retrouve dans le module les composants (badge, button, etc.) et les tokens de fondation (dans le dossier token) : 

tosk
├── build
├── src
│   ├── androidTest
│   ├── main
│   │   └── java
│   │       └── com
│   │           └── cheatshqip
│   │               └── tosk
│   │                   ├── badge
│   │                   ├── button
│   │                   ├── card
│   │                   ├── chip
│   │                   ├── textfield
│   │                   ├── tokens
│   │                   │   ├── primitive
│   │                   │   └── semantic
│   │                   ├── topappbar
│   │                   └── ToskTheme.kt
│   │
│   │── AndroidManifest.xml
│
└── test [unitTest]

J’aurais pu appeler le dossier tokens autrement, comme foundation. J’ai choisi de suivre ce qui se faisait sur backpack pour démarrer.

J’ai choisi aussi de séparer dans chaque composant la partie rendu en tant que tel et les tokens de composants : 

chip
├── tokens
│   ├── ToskChipColor
│   ├── ToskChipSize
│   └── ToskChipTextStyle
└── ToskChip.kt

Ici, ToskChip contient la définition en Jetpack Compose du composant Chip de mon design system. Il y a trois tokens de composant : la couleur, la taille et le textstyle. En termes de rendu, une chip est violette lorsqu’elle est sélectionnée, et grise lorsqu’elle ne l’est pas : 

maquette de mon application

Ici finalement on voit de nombreux composants : Bouton, Chips, Badge, TextView, Card, ImageButton : 

composants dans la maquette

Je me suis basé sur cet exemple pour montrer l’implémentation réalisée, en partant de la définition du thème, puis des tokens primitifs, pour arriver au composant. On visualisera les composants dans des Previews mais aussi dans une application démo.

Application démo

Même si Jetpack Compose offre des Preview, créer une application démo aide à se rendre compte du rendu final des composants dans des conditions d’utilisation plus standards. En se projetant, cette application enfile aussi le rôle de documentation vivante pour les développeurs et les designers qui utilisent le Design System.

Comme pour Tosk, j’ai fait un module application séparé :

toskdemo
├── build
├── src
│   ├── androidTest
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── cheatshqip
│   │   │           ├── MainActivity.java
│   │   │           └── MainScreen.kt
│   │   ├── res
│   │   └── AndroidManifest.xml
│   └── test   # [unitTest]

Pour démarrer, j’ai choisi d’ajouter les composants un par un dans un composant parent MainScreen : 

@Composable
fun MainScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(ToskSpacing.M)
            .verticalScroll(rememberScrollState()),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        ToskButton(
            enabled = true,
            color = ToskButtonColor.secondary(),
            contentDescription = "Singular Definite",
            onClick = { },
        ) { Text("Singular Definite") }
        Spacer(modifier = Modifier.padding(ToskSpacing.M))
        ToskButton(
            enabled = true,
            contentDescription = "Singular Indefinite",
            onClick = { },
        ) { Text("Singular Indefinite") }
        /* Autres composants */
    }
}

Ces composants sont séparés par un Spacer. L’objectif est de tous les visualiser via un simple scroll. Lorsque le nombre de composants augmentera, je les rangerai probablement en fonction de leur typologie (bouton, texte, etc.).

Définition du thème

La définition du thème se fait en plusieurs étapes. Tout d’abord, il faut définir des CompositionLocal, en l'occurrence pour les couleurs et la Typography (types et tailles de texte comme les headers, caption ou encore body).

private val LocalToskTypography = staticCompositionLocalOf<ToskTypography> {
    error("Wrap you content with ToskTheme {} to get access to Tosk typography")
}
private val LocalToskColors = staticCompositionLocalOf<ToskColors> {
    error("Wrap you content with ToskTheme {} to get access to Tosk colors")
}

J’utilise ici la fonction staticCompositionLocalOf<T>, qui présente comme principale différence avec compositionLocalOf<T> le fait que son contenu n’est pas observé par JetpackCompose. C’est alors plus performant uniquement si le contenu défini ne change quasiment jamais. C’est à proscrire s’il change souvent puisque chaque changement déclenche la recréation de l’arbre Compose où la CompositionLocal est utilisée.

Ici, une  erreur est lancée si la CompositionLocal est utilisée en dehors de notre thème.

Justement, voici comment la fonction composable le définit : 

@ReadOnlyComposable
@Composable
@ReadOnlyComposable
@Composable
private fun lightDarkColors(): ToskColors {
    return if (isSystemInDarkTheme()) {
        ToskColors.Dark
    } else {
        ToskColors.Light
    }
}
@Composable
fun ToskTheme(
    content: @Composable () -> Unit,
) {
    val typography = ToskTypography.Default
    val colors = lightDarkColors()
    CompositionLocalProvider(
        LocalToskTypography provides typography,
        LocalToskColors provides colors,
        content = content,
    )
}

L’injection dans le CompositionLocalProvider global sert en revanche à rendre tous les tokens disponibles dans l’arbre compose où ToskTheme est injecté. J’ai recréé une fonction utilitaire lightDarkColors() pour déterminer dynamiquement les couleurs à injecter en fonction du mode Light / Dark défini pour le système. isSystemInDarkTheme() est fourni par Jetpack Compose. Dans cette implémentation, je n’ai pas rendu mes CompositionLocal surchargeables puisqu’elles sont privées. L’intérêt d’utiliser CompositionLocal peut alors se discuter.

Enfin, il faut créer un objet qui encapsule l’accès aux tokens du thème :

object ToskTheme {
    val typography: ToskTypography
        @Composable
        @ReadOnlyComposable
        get() = if (LocalInspectionMode.current) {
            ToskTypography.Default
        } else {
            LocalToskTypography.current
        }
    val colors: ToskColors
        @Composable
        @ReadOnlyComposable
        get() = if (LocalInspectionMode.current) {
            lightDarkColors()
        } else {
            LocalToskColors.current
        }
    val spacing: ToskSpacing
        @ReadOnlyComposable
        @Composable
        get() = ToskSpacing
}

Grâce à cette notation, on peut accéder aux valeurs du thème dans nos composables en écrivant par exemple ToskTheme.spacing.montoken.

Autre approche

Cette implémentation est largement inspirée de Backpack. Elle offre une grande versatilité puisque le thème est redéfini à la main de zéro. C’est aussi possible d’implémenter une instance de MaterialTheme. C’est plus rapide, mais c’est aussi plus contraignant puisque vous devez respecter l’API fournie par google :

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colors = myColorPalette,
        typography = myTypography,
        shapes = myShapes,
        content = content
    )
}

La philosophie est la même : les Shapes, Colors et Typography sont les tokens à implémenter. Le découpage est moindre, puisque par défaut tokens primitifs et sémantiques sont mélangés.

Tokens primitifs et sémantiques de mon design system Tosk

tokens
├── primitive
│   ├── ToskBorderRadius
│   ├── ToskBorderSize
│   ├── ToskElevation
│   ├── ToskFontSize
│   ├── ToskLetterSpacing
│   ├── ToskLineHeight.kt
│   ├── ToskPalette
│   ├── ToskRippleAlpha
│   ├── ToskSpacing 
└── semantic
    ├── ToskColors.kt
    ├── ToskShape
    └── ToskTypography.kt

J’ai défini beaucoup de composants, dont le Ripple qui correspond au changement de couleur observé lors d’un clic sur un bouton. J’ai choisi de les rendre accessible à travers un objet à chaque fois :

package com.cheatshqip.tosk.tokens.primitive

import androidx.compose.ui.graphics.Color

object ToskPalette {
    val jet: Color = Color(0xFF343434)
    val charcoal: Color = Color(0xFF36454F)
    val black = Color(0xFF000000)
    val slateGray = Color(0xFF4B5563)
    val silver = Color(0xFF9CA3AF)
    val purple = Color(0xFF7E22CE)
    val fireBrick = Color(0xFFB91C1B)
    /.../
}

Cela contraint le code appelant à appeler l’objet pour accéder aux valeurs désirées. C’est bien sûr plus verbeux mais j’aime bien avoir cette visibilité. Comme par exemple ici avec les tokens semantic ToskColors :

sealed class ToskColors(
    val text: ToskColorsText,
    val background: ToskColorsBackground,
    val border: ToskColorsBorder,
    val ripple: ToskRipple = ToskRipple
) {
    data object Light : ToskColors(
        text = ToskColorsText(
            primary = ToskPalette.black,
            textOnPrimary = ToskPalette.alabaster,
            textOnPrimaryDisabled = ToskPalette.coralReef,
            secondary = ToskPalette.slateGray,
            textOnSecondaryDisabled = ToskPalette.silver,
            accent = ToskPalette.purple,
            textOnAccentDisabled = ToskPalette.silver,
            textOnInfo1 = ToskPalette.fireBrick,
            textOnInfo2 = ToskPalette.royalBlue,
            textOnInfo3 = ToskPalette.forestGreen,
            textOnInfo4 = ToskPalette.brown,
            textOnInfo5 = ToskPalette.rust,
            error = ToskPalette.crimson,
        ),
        background = ToskColorsBackground(
            primary = ToskPalette.crimson,
            primaryDisabled = ToskPalette.salmonPink,
            secondary = ToskPalette.alabaster,
            secondaryDisabled = ToskPalette.lightGray,
            accent = ToskPalette.lavender,
            accentDisabled = ToskPalette.lightGray,
            info1 = ToskPalette.mistyRose,
            info2 = ToskPalette.aliceBlue,
            info3 = ToskPalette.honeydew,
            info4 = ToskPalette.lemonChiffon,
            info5 = ToskPalette.papayaWhip,
        ),
        border = ToskColorsBorder(
            primary = ToskPalette.pinkLace,
            secondary = ToskPalette.lightGray,
            accent = ToskPalette.mauve,
        )
    )
    data object Dark : ToskColors(
        text = ToskColorsText(
            primary = ToskPalette.alabaster,
            textOnPrimary = ToskPalette.alabaster,
            textOnPrimaryDisabled = ToskPalette.alabaster,
            secondary = ToskPalette.alabaster,
            textOnSecondaryDisabled = ToskPalette.alabaster,
            accent = ToskPalette.purple,
            textOnAccentDisabled = ToskPalette.alabaster,
            textOnInfo1 = ToskPalette.salmonPink,
            textOnInfo2 = ToskPalette.aliceBlue,
            textOnInfo3 = ToskPalette.honeydew,
            textOnInfo4 = ToskPalette.lemonChiffon,
            textOnInfo5 = ToskPalette.papayaWhip,
            error = ToskPalette.crimson,
        ),
        background = ToskColorsBackground(
            primary = ToskPalette.crimson,
            primaryDisabled = ToskPalette.fireBrick,
            secondary = ToskPalette.charcoal,
            secondaryDisabled = ToskPalette.jet,
            accent = ToskPalette.darkPurple,
            accentDisabled = ToskPalette.jet,
            info1 = ToskPalette.maroon,
            info2 = ToskPalette.navy,
            info3 = ToskPalette.darkGreen,
            info4 = ToskPalette.darkGoldenrod,
            info5 = ToskPalette.saddleBrown,
        ),
        border = ToskColorsBorder(
            primary = ToskPalette.fireBrick,
            secondary = ToskPalette.jet,
            accent = ToskPalette.darkPurple,
        )
    )
}

J’aime l’approche des sealed classes puisque le compilateur nous aide en cas d’énumération dans un when. J’aime aussi cette approche puisque cela crée un ensemble cohérent de structures. Pour mieux ranger les couleurs, je les ai rangées en fonction du type de couleur cible : pour une bordure, un arrière-plan, un texte ou un ripple. Chez Backpack ou Material on parlera plutôt de Surface que de background.

J’ai aussi fait apparaître les variants Dark et Light pour que l’application puisse adapter dynamiquement ses couleurs à l’appréciation des utilisateurs. 

ToskShape et ToskTypography suivent le même principe : packager des structures fournies par Jetpack Compose (Color, Shape, TextStyle…) dans des sealed class en utilisant des tokens primitifs :

sealed class ToskShape : Shape {
    data object Medium : ToskShape(), Shape by RoundedCornerShape(ToskBorderRadius.Medium)
}
val defaultFontFamily = FontFamily.SansSerif
sealed class ToskTypography(
    val body: TextStyle,
    val caption: TextStyle,
    val footnote: TextStyle,
    val heading1: TextStyle,
    val heading2: TextStyle,
    val heading3: TextStyle,
    val heading4: TextStyle,
    val heading5: TextStyle,
    val hero1: TextStyle,
    val hero2: TextStyle,
    val hero3: TextStyle,
    val hero4: TextStyle,
    val hero5: TextStyle,
    val label1: TextStyle,
    val label2: TextStyle,
    val label3: TextStyle,
    val subheading: TextStyle,
) {
    data object Default : ToskTypography(
        body = TextStyle(
            fontWeight = FontWeight.Normal,
            fontSize = ToskFontSize.M,
            lineHeight = ToskLineHeight.M,
            fontFamily = defaultFontFamily,
            lineHeightStyle = defaultLineHeightStyle(),
        ),
        caption = TextStyle(
            fontWeight = FontWeight.Normal,
            fontSize = ToskFontSize.XS,
            lineHeight = ToskLineHeight.XS,
            fontFamily = defaultFontFamily,
            lineHeightStyle = defaultLineHeightStyle(),
        ),
        /../
    )
}

Il faut évidemment utiliser les structures de base de Compose à un moment pour les intégrer aux composants graphiques utilisés.

Components et components tokens 

J’ai aussi défini des composants pour m’accélérer. Voici par exemple la structure du répertoire Button : 

button
├── tokens
│   ├── ToskButtonColor
│   ├── ToskButtonSize
│   └── ToskButtonTextStyle
└── ToskButton.kt

Le sous-dossier tokens contient les définitions propres au composant. Ces component tokens utilisent semantic et primitive tokens, comme  ToskButtonSize et ToskButtonTextStyle : 

private const val MINIMUM_BUTTON_HEIGHT
 = 40
sealed class ToskButtonSize(
    val minHeight: Dp,
    val contentPadding: PaddingValues = PaddingValues(
        vertical = ToskSpacing.None,
        horizontal = ToskSpacing.M
    )
) {
    data object Medium : ToskButtonSize(minHeight = MINIMUM_BUTTON_HEIGHT
.dp)
}
object ToskButtonTextStyle {
    val default: TextStyle
        @Composable
        get() = ToskTheme.typography.heading5
}

Accessibilité

Vous aurez remarqué que cette première version ne présente pas les configurations nécessaires à l’accessibilité. Il faut garder en tête que c’est préférable d’inclure ce paramétrage dès le début. De ce côté-ci, nous sommes chanceux puisque la documentation fournie par Android est riche.

Je vous propose aussi de vous référer à l’article d’une collègue qui traite précisément du sujet.

Autres difficultés

Les exemples de Design System mobiles open source sont rares, et la documentation aussi. La compréhension des types de tokens m’a vraiment été facilitée par le livre TheDesignTokensBook. Les exemples sont pertinents et nombreux.

Plus concrètement, j’ai rencontré des difficultés sur la création de mon champ TextField. Au départ, je voulais faire un wrapping des composants fournis par Material Design. Les APIs ne sont pas vraiment sèches. Je ne pouvais pas personnaliser facilement la taille des bordures…et finalement j’ai personnalisé le composant BasicTextField, au détriment de la concision de mon implémentation.

Conclusion

Pour l’état actuel de mon implémentation, j’ai clairement dépensé plus de temps à la réalisation de ce Design System que si j’avais développé sans convention. Mais son utilisation allège déjà les écrans. L’intérêt du Design System est bien sûr décuplé avec l’augmentation de la taille de l’équipe et de votre base de code. Cela pousse aussi à mieux structurer les maquettes et les échanges avec les autres acteurs du projet, que ce soit les profils UX ou Produit. Comme d’autres pratiques telles que la non régression ou l’accessibilité, cela coûtera moins cher de l’anticiper et de le faire au fil de l’eau que de le décider après coup. Si vous êtes dans ce second cas, commencez par faire un kit de composants, et enrichissez petit à petit l’ensemble. Vous aurez sinon des points de congestion (et de friction) le temps que les spécifications UI et UX soient prêtes et validées. Comme pour d’autres pratiques, j’ai surtout passé du temps à comparer les approches existantes pour me définir une cible. Si vous savez où aller, l’investissement sera d’autant moins coûteux.

Enfin, pour que ce Design System soit un succès, il faudra bien veiller à ce que les conventions spécifiées soient respectées, et que les composants définis soient réutilisés au maximum. Trop souvent malheureusement les maquettes sont implémentées telles qu’elles sont reçues, alors qu’une phase d’affinage serait nécessaire. Pour cela, les techs et UX/UI doivent collaborer. Les uns ne doivent pas être les consommateurs des autres.

Sources