CompositionLocal
est un outil permettant de transmettre des données implicitement via la composition. Sur cette page, vous allez découvrir CompositionLocal
de manière plus détaillée, apprendre à créer votre propre CompositionLocal
et déterminer si CompositionLocal
est une solution adaptée à votre cas d'utilisation.
Découvrez CompositionLocal
Généralement dans Compose, les données descendent dans l'arborescence de l'interface utilisateur en tant que paramètres pour chaque fonction modulable. Les dépendances d'un composable deviennent donc explicites. Cela peut toutefois s'avérer fastidieux pour les données très fréquemment et couramment utilisées, telles que les couleurs ou les styles de type. Consultez l'exemple suivant :
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
Pour vous éviter de devoir transmettre les couleurs en tant que dépendance de paramètre explicite à la plupart des composables, Compose propose CompositionLocal
. Cela vous permet de créer des objets nommés limités à l'arborescence qui peuvent être utilisés comme un moyen implicite pour faire circuler des données dans l'arborescence de l'interface utilisateur.
Les éléments CompositionLocal
sont généralement accompagnés d'une valeur dans un nœud spécifique de l'arborescence de l'interface utilisateur. Cette valeur peut être utilisée par ses descendants composables sans déclarer le CompositionLocal
en tant que paramètre dans la fonction modulable.
Le thème Material utilise CompositionLocal
en arrière-plan.
MaterialTheme
est un objet qui fournit trois instances CompositionLocal
: colorScheme
, typography
et shapes
, ce qui vous permet de les récupérer ultérieurement dans n'importe quelle partie descendante de la composition.
Plus précisément, il s'agit des propriétés LocalColorScheme
, LocalShapes
et LocalTypography
auxquelles vous pouvez accéder via les attributs colorScheme
, shapes
et typography
de MaterialTheme
.
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
Le champ d'application d'une instance de CompositionLocal
est limité à une partie de la composition afin que vous puissiez fournir différentes valeurs à différents niveaux de l'arborescence. La valeur current
d'un CompositionLocal
correspond à la valeur la plus proche fournie par un ancêtre dans cette partie de la composition.
Pour fournir une nouvelle valeur à un CompositionLocal
, utilisez le CompositionLocalProvider
et sa fonction infixe provides
, qui associe une clé CompositionLocal
à un value
. Le lambda content
de CompositionLocalProvider
obtient la valeur fournie lors de l'accès à la propriété current
de CompositionLocal
. Lorsqu'une nouvelle valeur est fournie, Compose recompose les parties de la composition qui lisent le CompositionLocal
.
À titre d'exemple, le CompositionLocal
LocalContentColor
contient la couleur de contenu préférée utilisée pour le texte et l'iconographie afin de s'assurer qu'elle contraste avec la couleur d'arrière-plan actuelle. Dans l'exemple suivant, CompositionLocalProvider
est utilisé pour fournir différentes valeurs pour différentes parties de la composition.
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. Surface { Column { Text("Uses Surface's provided content color") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { Text("Primary color provided by LocalContentColor") Text("This Text also uses primary as textColor") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { DescendantExample() } } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the error color now") }
Image 1. Aperçu du composable CompositionLocalExample
.
Dans le dernier exemple, les instances de CompositionLocal
ont été utilisées en interne par les composables Material. Pour accéder à la valeur actuelle d'un CompositionLocal
, utilisez sa propriété current
. Dans l'exemple suivant, la valeur Context
actuelle du LocalContext
CompositionLocal
généralement utilisée dans les applications Android est utilisée pour la mise en forme du texte :
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
Créer votre propre CompositionLocal
CompositionLocal
est un outil permettant de transmettre des données implicitement via la composition.
Un autre indicateur clé pour l'utilisation de CompositionLocal
est le cas où le paramètre est transversal et où les couches intermédiaires de la mise en œuvre ne doivent pas être informées de son existence. En effet, informer ces couches intermédiaires limiterait l'utilité du composable. Par exemple, l'interrogation des autorisations Android est accordée par un CompositionLocal
en arrière-plan. Un composable de sélecteur de fichiers multimédias peut ajouter une nouvelle fonctionnalité pour accéder au contenu protégé par des autorisations sur l'appareil, sans modifier son API ni exiger que les appelants du sélecteur de contenus soient informés de ce contexte supplémentaire utilisé dans l'environnement.
Cependant, CompositionLocal
n'est pas toujours la meilleure solution. Nous déconseillons l'utilisation excessive de CompositionLocal
, car cela présente quelques inconvénients :
CompositionLocal
rend le comportement d'un composable plus difficile à comprendre. Lorsqu'ils créent des dépendances implicites, les appelants de composables qui les utilisent doivent s'assurer qu'une valeur est respectée pour chaque CompositionLocal
.
De plus, il est possible qu'il n'y ait pas de référence claire pour cette dépendance, car elle peut être mutée dans n'importe quelle partie de la composition. Ainsi, déboguer l'application en cas de problème peut être plus difficile, car vous devez accéder à la composition pour voir où la valeur current
a été fournie. Des outils tels que Trouver des utilisations dans l'IDE ou Outil d'inspection de la mise en page de Compose fournissent suffisamment d'informations pour atténuer ce problème.
Décider d'utiliser CompositionLocal
CompositionLocal
peut être une bonne solution pour votre cas d'utilisation dans certaines conditions :
Un CompositionLocal
doit disposer d'une bonne valeur par défaut. En l'absence de valeur par défaut, vous devez garantir qu'il est très difficile pour un développeur de se retrouver dans une situation où aucune valeur n'est fournie pour CompositionLocal
.
Si vous ne fournissez pas de valeur par défaut, vous risquez de rencontrer des problèmes lors de la création de tests. De même, la prévisualisation d'un composable qui utilise ce CompositionLocal
exigera toujours qu'il soit explicitement fourni.
Évitez CompositionLocal
pour les concepts qui ne sont pas considérés comme limités à l'arborescence ou aux sous-hiérarchies. Un CompositionLocal
convient lorsqu'il peut être utilisé par n'importe quel descendant et non seulement par quelques-uns d'entre eux.
Si votre cas d'utilisation ne répond pas à ces exigences, consultez la section Alternatives à prendre en compte avant de créer un CompositionLocal
.
Une pratique déconseillée consiste par exemple à créer un CompositionLocal
qui contient le ViewModel
d'un écran particulier, afin que tous les composables de cet écran puissent obtenir une référence au ViewModel
pour exécuter une logique. Cette pratique est déconseillée, car tous les composables sous une arborescence d'interface utilisateur particulière n'ont pas besoin d'être informés de l'existence d'un ViewModel
. Il est recommandé de ne transmettre aux composables que les informations dont ils ont besoin en suivant le schéma : les états descendent et les événements remontent. Cette approche facilitera la réutilisation et les tests de vos composables.
Créer un CompositionLocal
Deux API permettent de créer un CompositionLocal
:
compositionLocalOf
: la modification de la valeur fournie lors de la recomposition invalide uniquement le contenu qui lit sa valeurcurrent
.staticCompositionLocalOf
: contrairement àcompositionLocalOf
, les lectures d'unstaticCompositionLocalOf
ne sont pas suivies par Compose. La modification de la valeur entraîne la recomposition de l'intégralité du lambdacontent
contenant leCompositionLocal
au lieu des emplacements où la valeurcurrent
est lue dans la composition.
Si la valeur fournie au CompositionLocal
est peu susceptible de changer ou ne changera jamais, utilisez staticCompositionLocalOf
pour obtenir des avantages en termes de performances.
Par exemple, il est possible que le système de conception d'une application soit défini en fonction de l'élévation des composables à l'aide d'une ombre pour le composant d'interface utilisateur. Étant donné que les différentes élévations de l'application doivent se propager dans l'arborescence de l'interface utilisateur, nous utilisons un CompositionLocal
. Comme la valeur de CompositionLocal
est dérivée de manière conditionnelle en fonction du thème du système, nous utilisons l'API compositionLocalOf
:
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
Fournir des valeurs à un CompositionLocal
Le composable CompositionLocalProvider
associe des valeurs aux instances de CompositionLocal
pour la hiérarchie donnée. Pour fournir une nouvelle valeur à un CompositionLocal
, utilisez la fonction infixe provides
qui associe une clé CompositionLocal
à un value
comme suit :
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
Consommation du CompositionLocal
CompositionLocal.current
renvoie la valeur fournie par le CompositionLocalProvider
le plus proche qui fournit une valeur à ce CompositionLocal
:
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
Alternatives à prendre en compte
Un CompositionLocal
peut être une solution excessive dans certains cas d'utilisation. Si votre cas d'utilisation ne répond pas aux critères spécifiés dans la section Décider d'utiliser CompositionLocal, une autre solution est peut-être plus adaptée.
Transmettre des paramètres explicites
Indiquer explicitement les dépendances du composable est une bonne habitude à prendre. Nous vous recommandons de transmettre aux composables uniquement ce dont ils ont besoin. Pour encourager la dissociation et la réutilisation des composables, chaque composable doit contenir le moins d'informations possible.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
Inversion du contrôle
Un autre moyen d'éviter de transmettre des dépendances inutiles à un composable consiste à utiliser l'inversion du contrôle. L'exécution d'une logique à l'aide d'une dépendance est assurée par le parent au lieu du descendant.
Consultez l'exemple suivant, dans lequel un descendant doit déclencher la requête pour charger des données :
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
Selon le cas, MyDescendant
peut avoir beaucoup de responsabilités. De plus, la transmission de MyViewModel
en tant que dépendance rend MyDescendant
moins réutilisable, car ils sont désormais associés. Prenons l'alternative qui ne transmet pas la dépendance au descendant et qui utilise les principes d'inversion du contrôle rendant l'ancêtre responsable de l'exécution de la logique :
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Cette approche peut être plus adaptée à certains cas d'utilisation, car elle dissocie l'enfant de ses ancêtres immédiats. Les composables ancêtres ont tendance à devenir plus complexes au profit de composables de niveau inférieur plus flexibles.
De même, les lambdas de contenu @Composable
peuvent être utilisés de la même manière pour obtenir les mêmes avantages :
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Anatomie d'un thème dans Compose
- Utiliser les vues dans Compose
- Kotlin pour Jetpack Compose