CompositionLocal ist ein Tool, mit dem Daten implizit durch die Komposition weitergegeben werden können. Auf dieser Seite erfahren Sie mehr darüber, was ein CompositionLocal ist, wie Sie einen eigenen CompositionLocal erstellen und ob ein CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist.
Einführung in CompositionLocal
In Compose werden Daten normalerweise über den UI-Baum als Parameter an jede zusammensetzbare Funktion weitergegeben. Dadurch werden die Abhängigkeiten einer zusammensetzbaren Funktion explizit gemacht. Dies kann jedoch bei Daten, die sehr häufig und weit verbreitet verwendet werden, wie Farben oder Schriftstile, mühsam sein. Sehen Sie sich folgendes Beispiel an:
@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 ) }
Damit die Farben nicht als explizite Parameterabhängigkeit an die meisten Composables übergeben werden müssen, bietet Compose CompositionLocal, mit dem Sie benannte Objekte mit Baumstruktur erstellen können, die als implizite Möglichkeit verwendet werden können, Daten durch die UI-Baumstruktur fließen zu lassen.
CompositionLocal-Elemente werden normalerweise mit einem Wert in einem bestimmten Knoten des UI-Baums bereitgestellt. Dieser Wert kann von den zusammensetzbaren Nachfolgern verwendet werden, ohne dass CompositionLocal als Parameter in der zusammensetzbaren Funktion deklariert werden muss.
Wichtige Begriffe: In diesem Leitfaden verwenden wir die Begriffe Komposition, UI-Baum und UI-Hierarchie. Obwohl sie in anderen Leitfäden möglicherweise synonym verwendet werden, haben sie unterschiedliche Bedeutungen:
- Die Komposition ist der Datensatz des Aufrufdiagramms von zusammensetzbaren Funktionen.
- Der UI-Baum oder die UI-Hierarchie ist der Baum von
LayoutNode, der durch den Kompositionsprozess erstellt, aktualisiert und verwaltet wird.
CompositionLocal wird vom Material Design im Hintergrund verwendet.
MaterialTheme ist ein Objekt, das drei CompositionLocal-Instanzen bereitstellt: colorScheme, typography und shapes. Sie können sie später in jedem untergeordneten Teil der Komposition abrufen.
Konkret sind dies die Attribute LocalColorScheme, LocalShapes und LocalTypography, auf die Sie über die Attribute MaterialTheme, colorScheme, shapes und typography zugreifen können.
@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 ) }
Eine CompositionLocal-Instanz ist auf einen Teil der Composition beschränkt, sodass Sie auf verschiedenen Ebenen des Baums unterschiedliche Werte angeben können. Der current-Wert eines CompositionLocal entspricht dem nächstgelegenen Wert, der von einem Ancestor in diesem Teil der Composition bereitgestellt wird.
Wenn Sie einen neuen Wert für einen CompositionLocal angeben möchten, verwenden Sie die CompositionLocalProvider und die Infix-Funktion provides, die einen CompositionLocal-Schlüssel mit einem value verknüpft. Der Lambda-Ausdruck content von CompositionLocalProvider erhält den angegebenen Wert, wenn auf das Attribut current von CompositionLocal zugegriffen wird. Wenn ein neuer Wert bereitgestellt wird, werden in Compose die Teile der Komposition neu zusammengesetzt, die den CompositionLocal lesen.
Beispielsweise enthält LocalContentColor CompositionLocal die bevorzugte Inhaltsfarbe für Text und Symbole, um sicherzustellen, dass sie sich von der aktuellen Hintergrundfarbe abheben. Im folgenden Beispiel wird CompositionLocalProvider verwendet, um unterschiedliche Werte für verschiedene Teile der Komposition bereitzustellen.
@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") }
CompositionLocalExample.Im letzten Beispiel wurden die CompositionLocal-Instanzen intern von Material-Kompositionen verwendet. Verwenden Sie das Attribut current, um auf den aktuellen Wert eines CompositionLocal zuzugreifen. Im folgenden Beispiel wird der aktuelle Context-Wert des LocalContext CompositionLocal, der häufig in Android-Apps verwendet wird, zum Formatieren des Texts verwendet:
@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) }
Eigene Funktion erstellen: CompositionLocal
CompositionLocal ist ein Tool, mit dem Daten implizit durch die Komposition weitergegeben werden können.
Ein weiteres wichtiges Signal für die Verwendung von CompositionLocal ist, wenn der Parameter übergreifend ist und Zwischenebenen der Implementierung nicht wissen sollten, dass er existiert, da dies die Nützlichkeit der zusammensetzbaren Funktion einschränken würde. Beispielsweise wird die Abfrage von Android-Berechtigungen im Hintergrund durch einen CompositionLocal ermöglicht. Ein Media-Auswahl-Composable kann neue Funktionen für den Zugriff auf berechtigungsgeschützte Inhalte auf dem Gerät hinzufügen, ohne die API zu ändern und ohne dass Aufrufer der Media-Auswahl diesen zusätzlichen Kontext aus der Umgebung kennen müssen.
CompositionLocal ist jedoch nicht immer die beste Lösung. Wir raten von einer übermäßigen Verwendung von CompositionLocal ab, da dies einige Nachteile mit sich bringt:
CompositionLocal erschwert es, das Verhalten einer zusammensetzbaren Funktion nachzuvollziehen. Da sie implizite Abhängigkeiten erstellen, müssen Aufrufer von Composables, die sie verwenden, dafür sorgen, dass für jedes CompositionLocal ein Wert vorhanden ist.
Außerdem gibt es möglicherweise keine eindeutige „Source of Truth“ für diese Abhängigkeit, da sie sich in jedem Teil der Komposition ändern kann. Daher kann das Debugging der App bei einem Problem schwieriger sein, da Sie in der Composition nach oben navigieren müssen, um zu sehen, wo der Wert current bereitgestellt wurde. Tools wie Verwendungen finden in der IDE oder der Compose Layout Inspector bieten genügend Informationen, um dieses Problem zu beheben.
Entscheiden, ob CompositionLocal verwendet werden soll
Es gibt bestimmte Bedingungen, die CompositionLocal zu einer guten Lösung für Ihren Anwendungsfall machen können:
Ein CompositionLocal sollte einen guten Standardwert haben. Wenn es keinen Standardwert gibt, müssen Sie dafür sorgen, dass es für Entwickler extrem schwierig ist, in eine Situation zu geraten, in der kein Wert für CompositionLocal angegeben wird.
Wenn Sie keinen Standardwert angeben, kann das beim Erstellen von Tests oder beim Anzeigen einer Vorschau einer zusammensetzbaren Funktion, die diese CompositionLocal verwendet, zu Problemen und Frustration führen, da sie immer explizit angegeben werden muss.
Vermeiden Sie CompositionLocal für Konzepte, die nicht als baumbezogen oder unterhierarchiebezogen gelten. Ein CompositionLocal ist sinnvoll, wenn er potenziell von allen untergeordneten Ressourcen verwendet werden kann, nicht nur von einigen.
Wenn Ihr Anwendungsfall diese Anforderungen nicht erfüllt, lesen Sie den Abschnitt Alternativen, bevor Sie eine CompositionLocal erstellen.
Ein Beispiel für eine schlechte Praxis ist das Erstellen eines CompositionLocal, das den ViewModel eines bestimmten Bildschirms enthält, sodass alle Composables auf diesem Bildschirm einen Verweis auf den ViewModel erhalten können, um eine Logik auszuführen. Das ist keine gute Vorgehensweise, da nicht alle Composables unter einer bestimmten UI-Struktur Informationen zu einem ViewModel benötigen. Es empfiehlt sich, nur die Informationen an Composables zu übergeben, die sie benötigen. Dabei gilt das Muster, dass Zustände nach unten und Ereignisse nach oben fließen.
Dadurch werden Ihre Composables wiederverwendbarer und leichter zu testen.
CompositionLocal erstellen
Es gibt zwei APIs zum Erstellen eines CompositionLocal:
compositionLocalOf: Wenn Sie den während der Neuzusammensetzung bereitgestellten Wert ändern, wird nur der Inhalt ungültig, der dencurrent-Wert liest.staticCompositionLocalOf: Im Gegensatz zucompositionLocalOfwerden Lesevorgänge einesstaticCompositionLocalOfnicht von Compose erfasst. Wenn Sie den Wert ändern, wird der gesamtecontent-Lambda-Ausdruck, in dem derCompositionLocal-Wert bereitgestellt wird, neu zusammengesetzt, nicht nur die Stellen, an denen dercurrent-Wert in der Composition gelesen wird.
Wenn sich der für CompositionLocal angegebene Wert mit hoher Wahrscheinlichkeit nicht oder nie ändert, verwenden Sie staticCompositionLocalOf, um die Leistung zu verbessern.
Das Designsystem einer App kann beispielsweise vorschreiben, wie Composables mithilfe eines Schattens für die UI-Komponente hervorgehoben werden. Da die verschiedenen Höhen für die App im gesamten UI-Baum weitergegeben werden sollen, verwenden wir ein CompositionLocal. Da der Wert CompositionLocal bedingt vom Systemdesign abgeleitet wird, verwenden wir die compositionLocalOf API:
// 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() }
Werte für CompositionLocal angeben
Das Compose-Element CompositionLocalProvider bindet Werte an CompositionLocal-Instanzen für die angegebene Hierarchie. Wenn Sie einen neuen Wert für einen CompositionLocal angeben möchten, verwenden Sie die Infix-Funktion provides, die einen CompositionLocal-Schlüssel so mit einem value verknüpft:
// 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 } } } }
CompositionLocal nutzen
CompositionLocal.current gibt den Wert zurück, der vom nächsten CompositionLocalProvider bereitgestellt wird, der einen Wert für diesen CompositionLocal bereitstellt:
@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 } }
Alternativen berücksichtigen
Ein CompositionLocal ist für manche Anwendungsfälle möglicherweise eine übertriebene Lösung. Wenn Ihr Anwendungsfall nicht die im Abschnitt Entscheidung, ob CompositionLocal verwendet werden soll angegebenen Kriterien erfüllt, ist eine andere Lösung wahrscheinlich besser für Ihren Anwendungsfall geeignet.
Explizite Parameter übergeben
Es ist eine gute Gewohnheit, die Abhängigkeiten einer zusammensetzbaren Funktion explizit anzugeben. Wir empfehlen, Kompositionen nur die benötigten Daten zu übergeben. Um die Entkopplung und Wiederverwendung von Composables zu fördern, sollte jedes Composable so wenig Informationen wie möglich enthalten.
@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 of Control
Eine weitere Möglichkeit, die Übergabe unnötiger Abhängigkeiten an eine zusammensetzbare Funktion zu vermeiden, ist die Verwendung der Inversion of Control. Anstatt dass das untergeordnete Element eine Abhängigkeit zum Ausführen einer Logik übernimmt, übernimmt das übergeordnete Element diese Aufgabe.
Im folgenden Beispiel muss ein untergeordnetes Element die Anfrage zum Laden von Daten auslösen:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
Je nach Fall kann MyDescendant eine große Verantwortung tragen. Außerdem wird MyDescendant durch die Übergabe von MyViewModel als Abhängigkeit weniger wiederverwendbar, da sie jetzt miteinander verknüpft sind. Sehen wir uns die Alternative an, bei der die Abhängigkeit nicht an den Nachfolger übergeben wird und die Prinzipien der Inversion of Control verwendet werden, die den Vorgänger für die Ausführung der Logik verantwortlich machen:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Dieser Ansatz kann für einige Anwendungsfälle besser geeignet sein, da er das untergeordnete Element von seinen unmittelbaren Vorgängern entkoppelt. Übergeordnete Composables werden tendenziell komplexer, um flexiblere untergeordnete Composables zu ermöglichen.
Auf ähnliche Weise können @Composable-Inhalts-Lambdas verwendet werden, um dieselben Vorteile zu erzielen:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Empfehlungen für Sie
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Aufbau eines Designs in Compose
- Ansichten in Compose verwenden
- Kotlin für Jetpack Compose