La couleur dynamique Material You dans Jetpack Compose : garder sa couleur de marque quand le fond d'écran gagne
dynamicColorScheme() remplace votre palette par une palette construite à partir du fond d'écran de l'utilisateur. Un guide pratique pour harmoniser plutôt que perdre ses couleurs de marque.
Activez la couleur dynamique dans une app Compose et la première chose que vous remarquez, c’est que votre marque disparaît. Le vert de Granyn, le bleu d’Hydrame — disparus, remplacés par ce que dynamicColorScheme() a dérivé du fond d’écran de l’utilisateur. Sur un fond d’écran bleu standard, c’est correct. Sur un fond d’écran orange ou magenta, une app dont toute l’identité visuelle est « celle qui est verte » ou « celle qui est bleue » ressemble maintenant à toutes les autres apps du tiroir d’applications. C’est Material You qui fonctionne comme prévu, et c’est aussi un vrai problème si la couleur est la façon dont un utilisateur reconnaît votre app au milieu des autres.
La solution n’est pas de désactiver la couleur dynamique — les utilisateurs qui ont opté pour un thème à l’échelle du système remarquent quand une app refuse de s’y accorder. La solution, c’est l’harmonisation : garder les neutres et les couleurs d’interaction dérivés du fond d’écran, mais ramener votre teinte de marque dans le schéma au lieu de la laisser se faire écraser silencieusement.
Ce que dynamicColorScheme donne réellement
Sur API 31+, dynamicLightColorScheme(context) et dynamicDarkColorScheme(context) lisent android.R.color.system_accent1 à system_accent3 — des palettes tonales que le système a extraites du fond d’écran — et renvoient un ColorScheme Material 3 complet. C’est pratique parce que vous obtenez un schéma garanti cohérent avec le reste de l’OS. C’est aussi un schéma qui n’a aucune connaissance de la couleur que votre app est censée être.
@Composable
fun AppTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (isSystemInDarkTheme()) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
else -> if (isSystemInDarkTheme()) DarkColorScheme else LightColorScheme
}
MaterialTheme(colorScheme = colorScheme, content = content)
}
C’est là que s’arrêtent la plupart des tutoriels, et c’est exactement le code qui a fait que l’icône en goutte d’eau d’Hydrame et sa couleur d’accent dans l’app ont cessé de correspondre sur la moitié des téléphones sur lesquels je l’ai testée.
Harmoniser plutôt que remplacer
androidx.core.graphics.ColorUtils — pas de dépendance supplémentaire, il est déjà dans core-ktx — a un blendHSL que vous pouvez utiliser pour déplacer une couleur de marque fixe vers le primary dynamique du schéma, par petits pas, jusqu’à ce qu’elle soit assez proche pour paraître native sans perdre son identité. Les propres directives de design de Material appellent ça l’« harmonisation » : prendre une couleur que le système ne possède pas (un rouge d’erreur, une teinte de marque, une série de visualisation de données) et la mélanger partiellement vers le rôle dynamique le plus proche pour qu’elle ne jure pas.
fun harmonize(designColor: Color, dynamicColor: Color, fraction: Float = 0.2f): Color {
val blended = ColorUtils.blendHSL(
designColor.toArgb(),
dynamicColor.toArgb(),
fraction,
)
return Color(blended)
}
Appliqué à Hydrame, dont la couleur de marque primaire est #3B82F6 :
val scheme = dynamicColorScheme(context)
val harmonizedAccent = harmonize(
designColor = Color(0xFF3B82F6),
dynamicColor = scheme.primary,
fraction = 0.15f,
)
À fraction = 0.15f, l’accent se lit toujours indéniablement comme le bleu d’Hydrame, mais il se place confortablement à côté de quel que soit le neutre tonal produit par le fond d’écran au lieu de le combattre. J’utilise la couleur harmonisée uniquement pour les éléments qui portent l’identité de marque — l’écho de l’icône de l’app dans l’app, l’anneau de progression de la goutte d’eau, les illustrations d’onboarding — et je laisse tout le reste (surfaces, texte, séparateurs, boutons) sur les rôles dynamiques propres au système. Harmoniser chaque couleur du schéma va à l’encontre du but même de la couleur dynamique ; l’objectif est un seul accent identifiable qui survit dans une palette par ailleurs native au système, pas une prise de contrôle complète par la marque.
Le repli en dessous d’API 31
Les deux tiers de la logique de repli ci-dessus tiennent en une seule branche when, mais ça vaut la peine d’être délibéré sur l’apparence du schéma pré-Android-12, parce que ce n’est pas un schéma dynamique dégradé — c’est votre seul schéma sur ces appareils, point final. Je garde un ColorScheme construit à la main par app, en utilisant lightColorScheme(primary = ..., secondary = ..., ...) alimenté par les valeurs de marque exactes déjà présentes dans les tokens de design de chaque app (le #22C55E de Granyn, le #F59E0B de Mintly, le #6366F1 de Subly), plutôt que d’essayer d’approximer ce qu’un schéma dynamique aurait produit. Sur ces appareils, le problème de couleur de marque au cœur de cet article n’existe pas — vous possédez toute la palette — donc ne dépensez pas d’effort à simuler un comportement dynamique que vous ne pouvez de toute façon pas obtenir.
Tester à travers les fonds d’écran, pas seulement clair et sombre
La couleur dynamique ajoute une dimension de test facile à sauter : thème clair, thème sombre, et maintenant une gamme de teintes de fond d’écran. Un écran qui paraît correct sur le fond d’écran par défaut d’un Pixel peut avoir un contraste illisible sur un fond d’écran saturé, parce que system_accent1 déplace la teinte et le chroma ensemble. Avant de publier un écran thématisé, je le vérifie contre au minimum un fond d’écran froid, un chaud, et un gris peu saturé — réglable depuis Paramètres → Fond d’écran et style → choisir une couleur, ce qui permet de forcer une teinte précise sans chercher une image correspondante. Si le ratio de contraste de votre accent harmonisé par rapport à scheme.surface descend sous 4,5:1 sur l’un de ces fonds, le fraction fixe ci-dessus est trop agressif pour cette plage de teintes et a besoin d’une vérification de contraste plutôt que d’une constante fixe :
fun harmonizeWithContrast(designColor: Color, scheme: ColorScheme, fraction: Float = 0.15f): Color {
val candidate = harmonize(designColor, scheme.primary, fraction)
return if (ColorUtils.calculateContrast(candidate.toArgb(), scheme.surface.toArgb()) >= 4.5) {
candidate
} else {
designColor // revenir à la couleur de marque non modifiée plutôt que d'expédier un contraste faible
}
}
À vérifier avant de publier
- Chaque écran qui porte une couleur de marque (échos d’icône, illustrations, graphiques) utilise une valeur harmonisée, ni le primary dynamique brut ni le hex de marque non modifié.
- Le schéma de repli pré-API-31 est un vrai
ColorSchemeajusté à la main, pas une copie du schéma dynamique avec des valeurs codées en dur. - Le contraste est vérifié contre au moins un fond d’écran saturé et un à faible chroma, pas seulement le fond par défaut.
- Les surfaces hors marque (arrière-plans, séparateurs, boutons par défaut) restent sur les rôles dynamiques du système sans y toucher — résistez à l’envie de tout harmoniser.
La couleur dynamique est l’une des rares fonctionnalités de la plateforme Android qui donne à une app l’impression d’appartenir à un téléphone précis plutôt qu’à un téléphone générique. Elle mérite d’être gardée. Elle ne mérite juste pas que vous y sacrifiiez votre couleur de marque.
// À lire aussi
D’autres notes du journal
L'API SplashScreen d'Android en 2026 : un démarrage à froid sans flash blanc
Un guide pratique de l'API SplashScreen d'Android — configuration du thème, les limites de taille de l'icône animée que personne ne lit, et keepOnScreenCondition pour les données pas encore prêtes.
L'API In-App Review d'Android : demander une note sans être pénible
Un guide pratique de l'API In-App Review de Google — comment elle fonctionne réellement, quand la déclencher, et pourquoi la popup classique « notez-nous » nuit discrètement à votre note sur le Play Store.
L'accessibilité sur Android en 2026 : une checklist TalkBack et sémantique Compose qui tient la route
Une checklist pratique d'accessibilité Android pour 2026 — TalkBack, sémantique Compose, zones tactiles et la passe de test que je fais sur chaque écran avant publication.