Thématisation Jetpack Compose

1. Introduction

Dans cet atelier de programmation, vous allez apprendre à utiliser les API de thématisation Jetpack Compose pour personnaliser le style de votre application. Nous verrons comment personnaliser les couleurs, les formes et la typographie pour qu'elles soient utilisées de manière cohérente dans l'ensemble de l'application, avec la prise en charge de plusieurs thèmes tels que le thème clair et le thème sombre.

Points abordés

Cet atelier de programmation traite des points suivants :

  • Présentation de Material Design et de sa personnalisation pour votre marque
  • Comment Compose met en œuvre le système Material Design
  • Comment définir et utiliser les couleurs, la typographie et les formes dans votre application
  • Comment appliquer un style aux composants
  • Comment gérer les thèmes clair et sombre

Objectifs de l'atelier

Dans cet atelier de programmation, nous allons personnaliser le style d'une application de lecture d'actualités. Nous commencerons avec une application sans style, puis appliquerons ce que nous allons apprendre pour personnaliser l'application et gérer les thèmes sombres.

Image montrant Jetnews, une application de lecture d'actualités, avant l'application de styles.

Image montrant Jetnews, une application de lecture d'actualités, après l'application de styles.

Image montrant Jetnews, une application de lecture d'actualités, avec le thème sombre.

Avant : application sans style

Après : application stylisée

Après : thème sombre

Conditions préalables

2. Configuration

Dans cette étape, vous téléchargerez le code que vous utiliserez pendant l'atelier. Il comprend une application de lecture d'actualités simple que nous allons styliser.

Ce dont vous avez besoin

Télécharger le code

Si git est installé, vous pouvez simplement exécuter la commande ci-dessous. Pour vérifier si git est installé, saisissez git --version dans le terminal ou la ligne de commande, et vérifiez qu'il s'exécute correctement.

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ThemingCodelabM2

Si vous n'avez pas git, cliquez sur le bouton ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :

Ouvrez le projet dans Android Studio, sélectionnez Fichier > Importer le projet, puis accédez au répertoire ThemingCodelabM2.

Le projet contient trois packages principaux :

  • com.codelab.theming.data Ce package contient des classes de modèle et des exemples de données. Vous ne devriez pas avoir besoin de modifier ce package pendant cet atelier de programmation.
  • com.codelab.theming.ui.start Ce package est le point de départ de cet atelier. Vous devez apporter toutes les modifications demandées dans cet atelier de programmation dans ce package.
  • com.codelab.theming.ui.finish Ce package correspond à l'état final de l'atelier de programmation, pour information.

Créer et exécuter l'application

L'application possède deux configurations d'exécution qui reflètent les états initial et final de l'atelier de programmation. Pour déployer le code sur votre appareil ou votre émulateur, sélectionnez une des deux configurations et appuyez sur le bouton d'exécution.

a43ae3c4fa75836e.png

L'application comprend également des aperçus de mise en page Compose. Lorsque vous accédez à Home.kt dans le package start ou finish et que vous ouvrez l'affichage Projet, plusieurs aperçus permettent d'effectuer des itérations rapides sur le code de l'interface utilisateur :

758a285ad8a6cd51.png

3. Thématisation Material

Jetpack Compose propose la mise en œuvre de Material Design, un système de conception complet permettant de créer des interfaces numériques. Les composants Material Design (boutons, cartes, boutons bascules, etc.) sont basés sur la thématisation Material, un moyen systématique de personnaliser Material Design pour mieux refléter la marque de votre produit. Un thème Material inclut les attributs couleur, typographie et forme. Si vous personnalisez ces attributs, ils seront automatiquement répercutés dans les composants que vous utilisez pour créer votre application.

Il est utile de comprendre la thématisation Material pour savoir comment thématiser vos applications Jetpack Compose. Voici une brève description des concepts. Si vous connaissez déjà bien la thématisation Material, vous pouvez passer à l'étape suivante.

Couleur

Material Design définit un certain nombre de couleurs nommées sémantiquement que vous pouvez utiliser dans votre application :

62ccfe5761fd9eda.png

La couleur Primary correspond à la couleur principale de votre marque, tandis que la couleur Secondary sert à fournir des accents. Vous pouvez indiquer des variantes plus sombres/plus claires pour les zones contrastées. Les couleurs d'arrière-plan et de surface sont utilisées pour les conteneurs contenant des composants qui résident en théorie sur une "surface" de votre application. Material définit également les couleurs "on", c'est-à-dire les couleurs à utiliser pour le contenu situé au-dessus de l'une des couleurs nommées. Par exemple, le texte d'un conteneur coloré en "Surface" doit être coloré en "OnSurface". Les composants Material sont configurés de manière à utiliser ces couleurs de thème. Par exemple, la couleur du bouton d'action flottant est définie par défaut sur secondary, la couleur du paramètre Fiches sur surface, etc.

Il est possible de fournir d'autres palettes de couleurs, comme un thème clair et un thème sombre, en définissant des couleurs nommées :

1a9b78141ddfa87b.png

Nous vous conseillons également de définir une petite palette de couleurs et de l'utiliser de façon cohérente dans l'ensemble de l'application. L'outil de sélection de couleur de Material peut vous aider à choisir les couleurs et à créer une palette de couleurs. Il s'assure même de l'accessibilité des combinaisons.

Typographie

De même, Material Design définit un certain nombre de styles de type nommés sémantiquement :

1d44de3ff2f7fd1c.png

Vous ne pouvez pas modifier les styles de type selon le thème, mais l'utilisation d'une échelle de type favorise la cohérence dans votre application. Le fait de fournir vos propres polices et d'autres personnalisations de type sera reflété dans les composants Material que vous utilisez dans votre application. Par exemple, les barres d'application utilisent le style h6 par défaut, les boutons utilisent button. Le générateur d'échelles de type Material peut vous aider à créer votre échelle de type.

Forme

Material Design prend en charge l'utilisation systématique de formes pour refléter votre marque. Il définit trois catégories : composants petits, moyens et grands. Chacune d'elles peut définir une forme à utiliser, en personnalisant le style d'angle (coupé ou arrondi) et la taille.

886b811cc9cad18e.png

La personnalisation de votre thème pour les formes sera reflétée sur de nombreux composants. Par exemple, les boutons et les champs de texte utilisent par défaut le thème pour les formes de petite taille, les fiches et les boîtes de dialogue le thème pour les formes de taille moyenne et les feuilles de calcul le thème pour les formes de grande taille. Pour consulter la liste complète des composants avec les thèmes de forme qui leur sont associés, cliquez ici. L'outil de personnalisation des formes Material peut vous aider à générer un thème pour les formes.

Référence

Material Design est défini par défaut sur un thème de référence, à savoir le jeu de couleurs violettes, l'échelle de type Roboto et les formes légèrement arrondies comme sur les images ci-dessus. Si vous ne spécifiez ou ne personnalisez pas votre thème, les composants utilisent le thème de référence.

4. Définir votre thème

MaterialTheme

L'élément principal de la mise en œuvre de la thématisation dans Jetpack Compose est le composable MaterialTheme. Le fait de placer ce composable dans votre hiérarchie Compose vous permet de définir la personnalisation de la couleur, du type et de la forme de tous les composants qu'il contient. Voici comment ce composable est défini dans la bibliothèque :

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) { ...

Vous pouvez ensuite récupérer les paramètres transmis dans ce composable à l'aide de MaterialTheme object, qui expose les propriétés de colors, typography et shapes. Nous aborderons ces points plus en détail plus bas.

Ouvrez Home.kt et localisez la fonction composable Home, qui est le point d'entrée principal de l'application. Notez que même si nous déclarons un MaterialTheme, nous ne spécifions aucun paramètre. Nous utilisons donc par défaut le "style de référence" :

@Composable
fun Home() {
  ...
  MaterialTheme {
    Scaffold(...

Créons des paramètres de couleur, de type et de forme pour appliquer un thème pour notre application.

Créer un thème

Pour centraliser votre style, nous vous recommandons de créer votre propre composable qui encapsule et configure un MaterialTheme. Vous définissez ainsi les personnalisations de votre thème à un seul et même endroit et pouvez les réutiliser facilement sur de nombreux écrans ou dans des @Preview. Vous pouvez créer plusieurs composables de thème si besoin, par exemple si vous souhaitez utiliser différents styles pour différentes sections de votre application.

Dans le package com.codelab.theming.ui.start.theme, créez un fichier appelé Theme.kt. Ajoutez une fonction composable appelée JetnewsTheme qui accepte d'autres composables comme contenu et encapsule un MaterialTheme :

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

Revenez maintenant à Home.kt et remplacez MaterialTheme par notre JetnewsTheme (et importez-le) :

-  MaterialTheme {
+  JetnewsTheme {
    ...

Vous ne remarquerez aucune modification immédiate dans la @Preview de cet écran. Mettez à jour PostItemPreview et FeaturedPostPreview pour encapsuler leur contenu avec notre nouveau composable JetnewsTheme, afin que les aperçus utilisent notre nouveau thème :

@Preview("Featured Post")
@Composable
private fun FeaturedPostPreview() {
  val post = remember { PostRepo.getFeaturedPost() }
+ JetnewsTheme {
    FeaturedPost(post = post)
+ }
}

Couleurs

Voici la palette de couleurs que nous souhaitons intégrer dans notre application (pour l'instant, nous allons utiliser une palette de couleurs claires, nous reviendrons au thème sombre bientôt) :

b2635ed3ec4bfc8f.png

Les couleurs de Compose sont définies à l'aide de la classe Color. Plusieurs constructeurs vous permettent de spécifier la couleur en tant que ULong ou par canal de couleur distinct.

Créez un fichier Color.kt dans votre package theme. Ajoutez les couleurs suivantes en tant que propriétés publiques de premier niveau dans ce fichier :

val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)

Maintenant que nous avons défini les couleurs de nos applications, rassemblons-les dans un objet Colors, requis par MaterialTheme, en attribuant des couleurs spécifiques aux couleurs nommées de Material. Revenez à Theme.kt et ajoutez les éléments suivants :

private val LightColors = lightColors(
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

Ici, nous utilisons la fonction lightColors pour créer nos Colors. Cela fournit des valeurs par défaut logiques, et il n'est donc pas nécessaire de spécifier toutes les couleurs qui composent une palette de couleurs Material. Par exemple, notez que nous n'avons pas spécifié de couleur de background, ni la plupart des couleurs "on". Nous utiliserons les valeurs par défaut.

Utilisons maintenant ces couleurs dans l'application. Mettez à jour votre composable JetnewsTheme pour utiliser nos Colors :

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
+   colors = LightColors,
    content = content
  )
}

Ouvrez Home.kt et actualisez l'aperçu. Notez le nouveau jeu de couleurs reflété dans les composants tels que la TopAppBar.

Typographie

Voici l'échelle de type que nous souhaitons intégrer dans notre application :

54c420f78529b77d.png

Dans Compose, nous pouvons définir des objets TextStyle, afin de définir les informations nécessaires pour appliquer un style à certains textes. Exemple de ses attributs :

data class TextStyle(
    val color: Color = Color.Unset,
    val fontSize: TextUnit = TextUnit.Inherit,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontFamily: FontFamily? = null,
    val letterSpacing: TextUnit = TextUnit.Inherit,
    val background: Color = Color.Unset,
    val textAlign: TextAlign? = null,
    val textDirection: TextDirection? = null,
    val lineHeight: TextUnit = TextUnit.Inherit,
    ...
)

L'échelle de type souhaitée utilise Montserrat pour les titres et Domine pour le corps du texte. Les fichiers de police pertinents sont déjà ajoutés au dossier res/fonts de votre projet.

Créez un fichier Typography.kt dans le package theme. Commençons par définir les FontFamily (qui combinent les différentes épaisseurs de chaque Font) :

private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)

Créez maintenant un objet Typography accepté par un MaterialTheme, en spécifiant des TextStyle pour chaque style sémantique de l'échelle :

val JetnewsTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)

Ouvrez Theme.kt et mettez à jour le composable JetnewsTheme pour utiliser notre nouvelle Typography :

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
+   typography = JetnewsTypography,
    content = content
  )
}

Ouvrez Home.kt et actualisez l'aperçu pour afficher la nouvelle typographie.

Formes

Nous souhaitons utiliser des formes pour caractériser notre marque dans l'application. Nous voulons appliquer une forme aux angles coupés à plusieurs éléments :

9b60c78a78c61570.png

Compose propose les classes RoundedCornerShape et CutCornerShape, qui vous permettent de définir votre thème pour les formes.

Créez un fichier Shape.kt dans le package theme et ajoutez les éléments suivants :

val JetnewsShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)

Ouvrez Theme.kt et mettez à jour le composable JetnewsTheme pour pouvoir utiliser ces Shapes :

@Composable
fun JetnewsTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = JetnewsTypography,
+   shapes = JetnewsShapes,
    content = content
  )
}

Ouvrez Home.kt et actualisez l'aperçu pour voir à quel point la Card, qui affiche l'article mis en avant, reflète le nouveau thème de forme appliqué.

Thème sombre

La compatibilité de votre application avec un thème sombre permet non seulement une intégration améliorée sur les appareils des utilisateurs (qui disposent d'un thème sombre global à partir d'Android 10), mais également une réduction de la consommation d'énergie et une réponse aux besoins en accessibilité. Material propose des conseils de conception pour créer un thème sombre. Voici une autre palette de couleurs que nous aimerions ajouter pour le thème sombre :

21768b33f0ccda5f.png

Ouvrez Color.kt et ajoutez les couleurs suivantes :

val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)

Ouvrez Theme.kt et ajoutez :

private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

À présent, mettez à jour JetnewsTheme :

@Composable
fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
+   colors = if (darkTheme) DarkColors else LightColors,
    typography = JetnewsTypography,
    shapes = JetnewsShapes,
    content = content
  )
}

Nous avons ajouté ici un nouveau paramètre pour indiquer s'il faut utiliser un thème sombre. Nous avons défini par défaut qu'il doit interroger le paramètre global de l'appareil. Cela nous offre une bonne valeur par défaut, et il est toujours facile de la remplacer si vous voulez qu'un écran en particulier soit toujours sombre, ou ne le soit jamais, ou si vous souhaitez créer une @Preview à thème sombre.

Ouvrez Home.kt et créez un aperçu pour le composable FeaturedPost qui s'affiche en mode sombre :

@Preview("Featured Post • Dark")
@Composable
private fun FeaturedPostDarkPreview() {
    val post = remember { PostRepo.getFeaturedPost() }
    JetnewsTheme(darkTheme = true) {
        FeaturedPost(post = post)
    }
}

Actualisez le volet d'aperçu pour afficher l'aperçu avec le thème sombre.

84f93b209ce4fd46.png

5. Utiliser des couleurs

À la dernière étape, nous avons vu comment créer votre propre thème afin de définir les couleurs, le type et les formes de votre application. Tous les composants Material utilisent ces personnalisations prêtes à l'emploi. Par exemple, le composable FloatingActionButton utilise par défaut la couleur secondary du thème, mais vous pouvez en définir une autre en spécifiant une valeur différente pour ce paramètre :

@Composable
fun FloatingActionButton(
  backgroundColor: Color = MaterialTheme.colors.secondary,
  ...
) {

Vous ne souhaitez pas toujours utiliser les paramètres par défaut. Cette section vous explique comment utiliser les couleurs dans votre application.

Couleurs brutes

Comme nous l'avons vu précédemment, Compose propose une classe Color. Vous pouvez les créer localement, les conserver dans un object, etc. :

Surface(color = Color.LightGray) {
  Text(
    text = "Hard coded colors don't respond to theme changes :(",
    textColor = Color(0xffff00ff)
  )
}

Color utilise un certain nombre de méthodes utiles, telles que copy, pour créer une couleur avec différentes valeurs alpha, rouges, vertes et bleues.

Couleurs du thème

Une approche plus flexible consiste à récupérer les couleurs de votre thème :

Surface(color = MaterialTheme.colors.primary)

Ici, nous utilisons le MaterialTheme object dont la propriété colors renvoie l'ensemble Colors dans le composable MaterialTheme. Cela signifie que nous pouvons prendre en charge différents styles en proposant différents ensembles de couleurs pour notre thème. Nous n'avons pas besoin de modifier le code de l'application. Par exemple, notre AppBar utilise la couleur primary et l'arrière-plan de l'écran est surface ; la modification des couleurs de thème est reflétée dans les composables suivants :

b0b0ca02b52453a7.png

253ab041d7ea904e.png

Étant donné que chaque couleur de notre thème est une instance Color, nous pouvons également facilement déduire les couleurs à l'aide de la méthode copy :

val derivedColor = MaterialTheme.colors.onSurface.copy(alpha = 0.1f)

Ici, nous créons une copie de la couleur onSurface, mais avec une opacité de 10 %. Cette approche garantit que les couleurs fonctionnent sous différents thèmes, ce qui peut ne pas être le cas lorsque des couleurs statiques sont codées en dur.

Couleurs de la surface et du contenu

De nombreux composants acceptent une paire de couleurs et des "couleurs de contenu" :

Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

Cela vous permet non seulement de définir la couleur d'un composable, mais aussi d'utiliser une couleur par défaut pour le "contenu", c'est-à-dire les composables qu'il contient. De nombreux composables utilisent cette couleur de contenu par défaut, par exemple, une couleur Text ou une teinte Icon. La méthode contentColorFor récupère la couleur "on" appropriée pour toutes les couleurs du thème. Par exemple, si vous définissez un arrière-plan primary, la valeur onPrimary est définie comme couleur du contenu. Si vous choisissez une couleur d'arrière-plan différente de celle du thème, vous devez fournir une couleur de contenu logique.

Surface(color = MaterialTheme.colors.primary) {
  Text(...) // default text color is 'onPrimary'
}
Surface(color = MaterialTheme.colors.error) {
  Icon(...) // default tint is 'onError'
}

Vous pouvez utiliser le LocalContentColor CompositionLocal pour récupérer la couleur qui contraste avec l'arrière-plan actuel :

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

Lorsque vous définissez la couleur des éléments, utilisez de préférence une Surface, car elle définit une valeur CompositionLocal de couleur de contenu appropriée. Méfiez-vous des appels Modifier.background directs qui ne définissent pas de couleur de contenu appropriée.

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
+  Row(
...

Actuellement, nos composants Header ont toujours un arrière-plan Color.LightGray. Le thème clair semble correct, mais les couleurs sont très contrastées par rapport à l'arrière-plan du thème sombre. Ils ne spécifient pas la couleur du texte en question. Par conséquent, héritez la couleur de contenu actuelle qui peut ne pas contraster avec l'arrière-plan :

7329ac6ead5097eb.png

Résolvons à présent ce problème. Dans le composable Header dans Home.kt, supprimez le modificateur background spécifiant la couleur codée en dur. À la place, encapsulez le Text dans une Surface avec une couleur dérivée du thème et spécifiez que le contenu doit être coloré en primary :

+ Surface(
+   color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
+   contentColor = MaterialTheme.colors.primary,
+   modifier = modifier
+ ) {
  Text(
    text = text,
    modifier = Modifier
      .fillMaxWidth()
-     .background(Color.LightGray)
      .padding(horizontal = 16.dp, vertical = 8.dp)
  )
+ }

Valeurs alpha du contenu

Bien souvent, nous voulons mettre en avant ou au contraire mettre en retrait des contenus pour communiquer sur leur importance et fournir une hiérarchie visuelle. Material Design recommande d'utiliser différents niveaux d'opacité pour représenter ces différents niveaux d'importance.

Jetpack Compose met cela en œuvre via LocalContentAlpha. Vous pouvez spécifier une valeur alpha du contenu pour une hiérarchie en fournissant une valeur pour ce CompositionLocal. Les composables enfants peuvent utiliser cette valeur. Par exemple, Text et Icon utilisent par défaut la combinaison de LocalContentColor ajustée pour utiliser LocalContentAlpha. Material spécifie des valeurs alpha standards (high, medium, disabled), qui sont modélisées par l'objet ContentAlpha. Notez que MaterialTheme utilise ContentAlpha.high par défaut pour LocalContentAlpha.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting a different content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(...)
    Text(...)
}

Il est ainsi facile de représenter l'importance des composants de manière cohérente.

Nous utiliserons les valeurs alpha de contenu pour clarifier la hiérarchie des informations dans l'article sélectionné. Dans Home.kt, dans le composable PostMetadata, mettez en valeur les métadonnées medium :

+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(
    text = text,
    modifier = modifier
  )
+ }

103ff62c71744935.png

Thème sombre

Comme nous l'avons vu, pour gérer des thèmes sombres dans Compose, il vous suffit de définir différents ensembles de couleurs et d'interroger les couleurs via le thème. Voici quelques exceptions :

Vous pouvez vérifier si votre exécution est dans un thème clair :

val isLightTheme = MaterialTheme.colors.isLight

Cette valeur est définie par les fonctions du générateur lightColors/blackColors.

Dans les thèmes Material et sombres, les surfaces dont les élévations sont plus élevées reçoivent des superpositions d'élévation (leur arrière-plan est éclairci). Ce comportement est automatique lorsque vous utilisez une palette de couleurs sombres :

Surface(
  elevation = 2.dp,
  color = MaterialTheme.colors.surface, // color will be adjusted for elevation
  ...

Nous pouvons observer ce comportement automatique dans notre application, à la fois dans les composants TopAppBar et Card que nous utilisons. Les élévations 4 dp et 1 dp sont utilisées par défaut afin d'éclaircir automatiquement l'arrière-plan dans le thème sombre et mieux communiquer cette élévation :

cb8c617b8c151820.png

Material Design suggère d'éviter les zones étendues de couleurs vives dans le thème sombre. Il est courant de colorer un conteneur avec une couleur primary dans le thème clair et une couleur surface dans les thèmes sombres. de nombreux composants utilisent cette stratégie par défaut, par exemple les barres d'application et la navigation inférieure. Pour faciliter l'implémentation, Colors propose une couleur primarySurface qui a exactement ce comportement et que ces composants utilisent par défaut.

Notre application définit actuellement la couleur de la barre d'application sur primary. Nous pouvons suivre ce conseil en le remplaçant par primarySurface ou simplement en supprimant ce paramètre par défaut. Dans le composable AppBar, modifiez le paramètre backgroundColor de TopAppBar :

@Composable
private fun AppBar() {
  TopAppBar(
    ...
-   backgroundColor = MaterialTheme.colors.primary
+   backgroundColor = MaterialTheme.colors.primarySurface
  )
}

6. Utiliser du texte

Lorsque nous utilisons du texte, nous utilisons le composable Text pour afficher le texte, TextField et OutlinedTextField pour saisir du texte et TextStyle pour appliquer un style unique à notre texte. Les AnnotatedString permettent d'appliquer plusieurs styles à du texte.

Comme nous l'avons vu avec les couleurs, les composants Material qui affichent du texte récupèrent les personnalisations de typographie de notre thème :

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

C'est un peu plus complexe que d'utiliser les paramètres par défaut, comme nous l'avons vu avec les couleurs. En effet, les composants n'affichent pas directement le texte, mais offrent des API d'emplacement qui vous permettent de transmettre un composable Text. Comment les composants définissent-ils un style de typographie pour un thème ? En arrière-plan, ils utilisent le composable ProvideTextStyle (qui utilise lui-même un CompositionLocal) pour définir un TextStyle "actuel". Le composable Text interroge par défaut ce style "actuel" si vous ne fournissez aucun paramètre textStyle concret.

Par exemple, à partir des classes Button et Text de Compose :

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

Styles de texte du thème

Comme avec les couleurs, il est préférable de récupérer les TextStyle depuis le thème actuel, ce qui vous encourage à utiliser un petit ensemble de styles cohérents et les rend plus faciles à gérer. MaterialTheme.typography récupère l'instance Typography définie dans votre composable MaterialTheme, ce qui vous permet d'utiliser les styles que vous avez définis :

Text(
  style = MaterialTheme.typography.subtitle2
)

Si vous devez personnaliser un TextStyle, vous pouvez le copy et remplacer les propriétés (il s'agit simplement d'une data class), ou bien le composable Text accepte un certain nombre de paramètres de style qui seront superposés à n'importe quel TextStyle :

Text(
  text = "Hello World",
  style = MaterialTheme.typography.body1.copy(
    background = MaterialTheme.colors.secondary
  )
)
Text(
  text = "Hello World",
  style = MaterialTheme.typography.subtitle2,
  fontSize = 22.sp // explicit size overrides the size in the style
)

Dans notre application, de nombreux emplacements appliquent automatiquement le TextStyle du thème. Par exemple, TopAppBar définit le style de son title avec h6, et ListItem définit le style du texte principal et secondaire avec subtitle1 et body2, respectivement.

Nous allons appliquer les styles typographiques du thème au reste de notre application. Définissez subtitle2 pour Header, et h6 pour le titre et body2 pour l'auteur et les métadonnées du texte de FeaturedPost :

@Composable
fun Header(...) {
  ...
  Text(
    text = text,
+   style = MaterialTheme.typography.subtitle2

45dbf11d6c1013a0.png

Plusieurs styles

Si vous devez appliquer plusieurs styles à un texte, vous pouvez utiliser la classe AnnotatedString pour appliquer le balisage, en ajoutant des SpanStyle à une plage de texte. Vous pouvez les ajouter de façon dynamique ou utiliser la syntaxe DSL pour créer du contenu :

val text = buildAnnotatedString {
  append("This is some unstyled text\n")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red text\n")
  }
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
}

Appliquez un style aux balises décrivant chaque article de notre application. Actuellement, elles utilisent le même style de texte que les autres métadonnées. Nous utiliserons le style de texte overline et une couleur d'arrière-plan pour les différencier. Dans le composable PostMetadata :

+ val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
+   background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
+ )
post.tags.forEachIndexed { index, tag ->
  ...
+ withStyle(tagStyle) {
    append(" ${tag.toUpperCase()} ")
+ }
}

3f504aaa0a94599a.png

7. Utiliser des formes

Tout comme pour la couleur et la typographie, la définition de votre thème pour les formes est reflétée dans les composants Material. Par exemple, les Button vont récupérer la forme définie pour les composants de petite taille :

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

Comme pour les couleurs, les composants Material utilisent des paramètres par défaut pour vérifier facilement la catégorie de forme utilisée par un composant ou pour fournir une alternative. Pour voir le mappage complet des composants aux catégories de forme, consultez la documentation.

Notez que certains composants utilisent des formes de thème modifiées en fonction de leur contexte. Par exemple, TextField utilise par défaut le thème pour les formes de petite taille, mais applique une taille zéro angle aux angles inférieurs :

@Composable
fun FilledTextField(
  // other parameters
  shape: Shape = MaterialTheme.shapes.small.copy(
    bottomStart = ZeroCornerSize, // overrides small theme style
    bottomEnd = ZeroCornerSize // overrides small theme style
  )
) {

1f5fa6cf1355e7a6.png

Formes de thème

Bien sûr, vous pouvez utiliser des formes lorsque vous créez vos propres composants en utilisant des composables ou des Modifier qui acceptent les formes, par exemple Surface, Modifier.clip, Modifier.background, Modifier.border, etc.

@Composable
fun UserProfile(
  ...
  shape: Shape = MaterialTheme.shapes.medium
) {
  Surface(shape = shape) {
    ...
  }
}

Ajoutons la thématisation des formes à l'image affichée dans PostItem. Nous allons lui appliquer la forme small du thème avec un clip Modifier pour couper l'angle supérieur gauche :

@Composable
fun PostItem(...) {
  ...
  Image(
    painter = painterResource(post.imageThumbId),
+   modifier = Modifier.clip(shape = MaterialTheme.shapes.small)
  )

2f989c7c1b8d9e63.png

8. "Styles" des composants

Compose ne permet pas explicitement d'extraire le style d'un composant tel qu'un style Android View ou CSS. Étant donné que tous les composants Compose ont été créés en langage Kotlin, d'autres moyens permettent d'atteindre le même objectif. Au lieu de cela, créez votre propre bibliothèque de composants personnalisés et utilisez-les dans votre application.

Nous avons déjà fait cela dans notre application :

@Composable
fun Header(
  text: String,
  modifier: Modifier = Modifier
) {
  Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary,
    modifier = modifier.semantics { heading() }
  ) {
    Text(
      text = text,
      style = MaterialTheme.typography.subtitle2,
      modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    )
  }
}

Le composable Header est essentiellement un Text stylisé que nous pouvons utiliser dans l'application.

Nous avons constaté que tous les composants sont basés sur des blocs de construction de niveau inférieur. Vous pouvez donc utiliser ces mêmes blocs pour personnaliser les composants de Material Design. Par exemple, nous avons remarqué que Button utilise le composable ProvideTextStyle pour définir un style de texte par défaut pour le contenu qui lui est transmis. Vous pouvez utiliser exactement le même mécanisme pour définir votre propre style de texte :

@Composable
fun LoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier
    ) {
        ProvideTextStyle(...) { // set our own text style
            content()
        }
    }
}

Dans cet exemple, nous créons notre propre "style" pour LoginButton en encapsulant la classe Button standard et spécifions certaines propriétés comme unbackgroundColor et un style de texte différents.

Il n'existe pas non plus de concept de style par défaut, qui permet de personnaliser l'apparence par défaut d'un type de composant. Là encore, vous pouvez atteindre cet objectif en créant votre propre composant qui encapsule et personnalise un composant de bibliothèque. Par exemple, imaginons que vous souhaitiez personnaliser la forme de tous les Button de votre application, sans modifier le thème des formes de petite taille, ce qui affecterait d'autres composants (autres que Button). Pour ce faire, créez votre propre composable et utilisez-le dans toute l'application :

@Composable
fun AcmeButton(
  // expose Button params consumers should be able to change
) {
  val acmeButtonShape: Shape = ...
  Button(
    shape = acmeButtonShape,
    // other params
  )
}

9. Félicitations

Félicitations, vous avez terminé cet atelier de programmation et défini le style d'une application Jetpack Compose !

Vous avez mis en œuvre un thème Material pour personnaliser la couleur, la typographie et les formes utilisées dans l'application afin de refléter votre marque et renforcer la cohérence. Vous avez également géré les thèmes clair et sombre.

Et maintenant ?

Consultez les autres ateliers de programmation du parcours Compose :

Complément d'informations

Exemples d'applications

  • Owl présentant plusieurs thèmes
  • Jetcaster présentant une thématisation dynamique
  • Jetsnack présentant la mise en œuvre d'un système de conception personnalisé

Documents de référence