Thématisation avec les styles

Vous pouvez créer vos applications de plusieurs manières à l'aide de styles. Votre choix dépend de la place de votre application par rapport à son adoption de Material Design :

  1. Système de conception entièrement personnalisé, n'utilisant pas Material Design
    • Recommandation : Définissez des styles de composants qui utilisent des valeurs du thème et exposez des paramètres de style sur les composants du système de conception.
  2. Utilisation de Material Design
    • Recommandation : Attendez l'adoption de Material pour l'intégrer aux styles. Utilisez des styles sur vos propres composants lorsque cela est possible.

La couche de style

Dans le modèle Compose traditionnel, la personnalisation repose souvent sur la substitution de jetons globaux (couleurs et typographie) fournis par MaterialTheme, ou sur l'encapsulation et la substitution des propriétés d'un composable de système de conception lorsque cela est possible. Parfois, certaines propriétés de la couche Material ne sont pas exposées via les sous-systèmes ou les paramètres, mais sont des valeurs par défaut codées en dur sur le composant lui-même.

Avec l'API Styles, il existe une nouvelle couche d'abstraction qui sert de pont entre les sous-systèmes et les composants : les styles.

couche Responsabilité Exemple
Valeurs du sous-système Valeurs nommées val Primary = Color(0xFF34A85E)
Styles atomiques Style qui ne modifie qu'une seule propriété val largeSizeAtomic = Style { size(100.dp, 40.dp) }
Styles de composants Configurations spécifiques aux composants Bouton avec un arrière-plan principal et une marge intérieure de 16 dp. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Composants Élément d'interface utilisateur fonctionnel qui utilise un style. Button(style = buttonStyle) { ... }
Diagramme montrant la thématisation avec des styles et la nouvelle introduction de calque
Figure 1. Exemple de composant et de la manière dont il accède aux styles d'un thème.

Styles atomiques ou monolithiques

Avec l'API Styles, vous pouvez décomposer un style en styles atomiques distincts. Au lieu de définir des styles complexes et spécifiques aux composants, comme baseButtonStyle, vous pouvez également créer de petits styles utilitaires à usage unique. Ils agissent comme vos "atomes".

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

Composition à l'aide de "then"

L'une des fonctionnalités puissantes de la nouvelle API Styles est l'opérateur then, qui vous permet de fusionner plusieurs objets Style. Vous pouvez ainsi créer un composant à l'aide de classes utilitaires atomiques.

Traditionnel (non atomique) :

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

Refactorisation atomique :

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

Adopter des styles dans votre système de conception

Tenez compte des options suivantes lorsque vous adoptez des styles dans votre système de conception, en fonction de l'endroit où se situe votre système de conception dans le spectre.

Système de conception personnalisé avec des styles

À prendre en compte lorsque : vous avez reçu un guide de marque complet qui n'est pas basé sur Material Design et vous ne prévoyez pas d'utiliser Material Design.

Stratégie : implémentez un système de conception entièrement personnalisé et exposez les styles dans le cadre du thème.

Cette option est le chemin personnalisé si vous n'utilisez pas Material comme langage principal de votre système de conception. Vous contournez MaterialTheme entièrement pour les définitions visuelles et vous avez déjà créé votre propre thème personnalisé. Vous créez un CompanyTheme qui sert de conteneur pour vos styles.

  • Fonctionnement : créez un objet CompanyTheme qui contient des objets Style pour chaque composant de votre système. Vos composants (encapsuleurs autour de la logique Material ou implémentations Box ou Layout personnalisées) utilisent directement ces styles et exposent un paramètre Style pour les consommateurs de votre système de conception.
  • La couche de style : les styles sont la définition principale de votre système de conception. Les jetons sont des variables nommées qui sont transmises à ces styles. Cela permet une personnalisation approfondie, par exemple en définissant des animations uniques pour les changements d'état (par exemple, en animant l'échelle et la couleur lors d'une pression).

Si vous créez votre propre thème personnalisé sans utiliser Material et que vous souhaitez adopter des styles, ajoutez votre liste de styles à votre thème. Vous pouvez ainsi accéder à vos styles de base depuis n'importe où dans votre projet.

  1. Créez une classe Styles qui stocke les différents styles de votre application et créez les valeurs par défaut. Par exemple, dans l'application Jetsnack, la classe est nommée JetsnackStyles :

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. Fournissez Styles dans le cadre de votre thème global et exposez des fonctions d'extension d'assistance sur StyleScope pour accéder aux sous-systèmes :

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. Accédez à JetsnackStyles dans votre composable :

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

Au-delà de l'adoption globale du thème, il existe d'autres stratégies pour intégrer Styles dans vos applications. Vous pouvez exploiter Styles en ligne pour des sites d'appel spécifiques ou utiliser des définitions statiques lorsque les fonctionnalités de thème complètes ne sont pas nécessaires. Styles ne doit pas être échangé de manière conditionnelle, sauf si le style entier est fondamentalement différent. Vous devez préférer accéder aux jetons dynamiques dans une définition visuelle plutôt que de basculer entre des objets de style distincts.