Daten auf lokaler Ebene mit CompositionLocal

CompositionLocal ist ein Tool zum impliziten Übergeben von Daten durch die Komposition. Auf dieser Seite erfahren Sie mehr über CompositionLocal, wie Sie Ihre eigene CompositionLocal erstellen und ob CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist.

Einführung in CompositionLocal

In Compose fließen Daten in der Regel als Parameter für jede zusammensetzbare Funktion durch die UI-Struktur. Dadurch werden die Abhängigkeiten einer zusammensetzbaren Funktion explizit. Dies kann jedoch bei Daten umständlich sein, die sehr häufig und weit verbreitet sind, z. B. Farben oder Schriftstile. Sehen Sie sich das folgende 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 zusammensetzbaren Funktionen übergeben werden müssen, bietet Compose CompositionLocal. Damit können Sie benannte Objekte mit Gültigkeitsbereich auf Strukturebene erstellen, die als implizite Möglichkeit verwendet werden können, Daten durch die UI-Struktur zu leiten.

CompositionLocal -Elemente werden in der Regel mit einem Wert in einem bestimmten Knoten der UI-Struktur bereitgestellt. Dieser Wert kann von den zusammensetzbaren Nachfolgern verwendet werden, ohne dass CompositionLocal als Parameter in der zusammensetzbaren Funktion deklariert werden muss.

CompositionLocal wird im Hintergrund vom Material-Design verwendet. MaterialTheme ist ein Objekt, das drei CompositionLocal-Instanzen bereitstellt: colorScheme, typography und shapes. So können Sie sie später in jedem untergeordneten Teil der Komposition abrufen. Konkret sind das die Eigenschaften LocalColorScheme, LocalShapes und LocalTypography, auf die Sie über die Attribute colorScheme, shapes und typography von MaterialTheme 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 Komposition beschränkt , sodass Sie auf verschiedenen Ebenen der Struktur unterschiedliche Werte angeben können. Der current Wert eines CompositionLocal entspricht dem nächstgelegenen Wert, der von einem Vorgänger in diesem Teil der Komposition bereitgestellt wird.

Verwenden Sie CompositionLocalProvider und die Infix-Funktion provides, um einen neuen Wert für CompositionLocal bereitzustellen. Dabei wird ein CompositionLocal-Schlüssel einem value zugeordnet. Das content Lambda von CompositionLocalProvider erhält den bereitgestellten Wert, wenn auf die current Eigenschaft von CompositionLocal zugegriffen wird. Wenn ein neuer Wert bereitgestellt wird, werden Teile der Komposition, die CompositionLocal lesen, neu zusammengesetzt.

Ein Beispiel dafür ist LocalContentColor CompositionLocal. Es enthält die bevorzugte Inhaltsfarbe für Text und Ikonografie, damit sie sich von der aktuellen Hintergrundfarbe abhebt. Im folgenden Beispiel wird CompositionLocalProvider verwendet, um für verschiedene Teile der Komposition unterschiedliche Werte 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")
}

Vorschau der zusammensetzbaren Funktion „CompositionLocalExample“.
Abbildung 1. Vorschau der zusammensetzbaren Funktion CompositionLocalExample.

Im letzten Beispiel wurden die CompositionLocal-Instanzen intern von zusammensetzbaren Material-Funktionen verwendet. Verwenden Sie die current Eigenschaft, um auf den aktuellen Wert von CompositionLocal, zuzugreifen. Im folgenden Beispiel wird der aktuelle Context-Wert von LocalContext CompositionLocal, der gängig 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 CompositionLocal erstellen

CompositionLocal ist ein Tool zum impliziten Übergeben von Daten durch die Komposition.

Ein weiteres wichtiges Signal für die Verwendung von CompositionLocal ist, wenn der Parameter übergreifend ist und Zwischenebenen der Implementierung nicht darüber informiert werden sollten. Andernfalls würde die Nützlichkeit der zusammensetzbaren Funktion eingeschränkt. Beispielsweise wird die Abfrage nach Android-Berechtigungen im Hintergrund durch ein CompositionLocal ermöglicht. Eine zusammensetzbare Funktion für die Media Picker kann neue Funktionen hinzufügen, um auf berechtigungsgeschützte Inhalte auf dem Gerät zuzugreifen, ohne die API zu ändern. Außerdem müssen Aufrufer der Media Picker nicht über diesen zusätzlichen Kontext informiert werden, der aus der Umgebung verwendet wird.

CompositionLocal ist jedoch nicht immer die beste Lösung. Wir raten davon ab, CompositionLocal zu häufig zu verwenden , da dies einige Nachteile mit sich bringt:

CompositionLocal erschwert die Nachvollziehbarkeit des Verhaltens einer zusammensetzbaren Funktion. Da sie implizite Abhängigkeiten erstellen, müssen Aufrufer von zusammensetzbaren Funktionen, die sie verwenden, dafür sorgen, dass ein Wert für jede CompositionLocal vorhanden ist.

Außerdem gibt es möglicherweise keine eindeutige Quelle der Wahrheit für diese Abhängigkeit, da sie sich in jedem Teil der Komposition ändern kann. Daher kann das Debuggen der App bei einem Problem schwieriger sein , da Sie in der Komposition nach oben navigieren müssen, um zu sehen, wo der current-Wert bereitgestellt wurde. Tools wie Find usages in der IDE oder der Compose-Layoutinspektor bieten genügend Informationen, um dieses Problem zu beheben.

Entscheiden, ob CompositionLocal verwendet werden soll

Unter bestimmten Bedingungen kann CompositionLocal eine gute Lösung für Ihren Anwendungsfall sein:

Eine CompositionLocal sollte einen guten Standardwert haben. Wenn kein Standardwert vorhanden ist, müssen Sie dafür sorgen, dass es für einen Entwickler äußerst schwierig ist, in eine Situation zu geraten, in der kein Wert für CompositionLocal bereitgestellt wird. Wenn kein Standardwert angegeben wird, kann dies zu Problemen und Frustration führen, da beim Erstellen von Tests oder beim Vorschauen einer zusammensetzbaren Funktion, die CompositionLocal verwendet, immer ein Wert explizit angegeben werden muss.

Vermeiden Sie CompositionLocal für Konzepte, die nicht als auf Strukturebene oder Unterhierarchieebene betrachtet werden. CompositionLocal ist sinnvoll, wenn es potenziell von jedem Nachfolger verwendet werden kann, nicht nur von einigen.

Wenn Ihr Anwendungsfall diese Anforderungen nicht erfüllt, lesen Sie den Abschnitt Alternativen berücksichtigen, bevor Sie CompositionLocal erstellen.

Ein Beispiel für eine schlechte Vorgehensweise ist das Erstellen von CompositionLocal, das das ViewModel eines bestimmten Bildschirms enthält, sodass alle zusammensetzbaren Funktionen auf diesem Bildschirm einen Verweis auf das ViewModel erhalten können, um eine bestimmte Logik auszuführen. Das ist eine schlechte Vorgehensweise, da nicht alle zusammensetzbaren Funktionen unterhalb einer bestimmten UI-Struktur ein ViewModel kennen müssen. Die gute Vorgehensweise besteht darin, nur die Informationen an zusammensetzbare Funktionen zu übergeben, die sie benötigen, und dabei dem Muster zu folgen, dass der Status nach unten und Ereignisse nach oben fließen. Mit diesem Ansatz werden Ihre zusammensetzbaren Funktionen wiederverwendbarer und einfacher zu testen.

CompositionLocal erstellen

Es gibt zwei APIs zum Erstellen von CompositionLocal:

  • compositionLocalOf:Wenn der während der Neuzusammensetzung bereitgestellte Wert geändert wird, wird nurder Inhalt ungültig gemacht, der dencurrent-Wert liest.

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf werden Lesevorgänge von staticCompositionLocalOf nicht von Compose erfasst. Wenn der Wert geändert wird, wird das gesamte content-Lambda, in dem CompositionLocal bereitgestellt wird, neu zusammengesetzt, nicht nur die Stellen, an denen der current-Wert in der Komposition gelesen wird.

Wenn sich der für die CompositionLocal bereitgestellte Wert höchstwahrscheinlich nicht oder nie ändert, verwenden Sie staticCompositionLocalOf, um die Leistung zu verbessern.

Beispielsweise kann das Designsystem einer App vorschreiben, wie zusammensetzbare Funktionen mithilfe eines Schattens für die UI-Komponente hervorgehoben werden. Da die verschiedenen Erhebungen für die App in der gesamten UI-Struktur weitergegeben werden sollen, verwenden wir CompositionLocal. Da der CompositionLocal-Wert bedingt basierend auf dem 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 bereitstellen

Die zusammensetzbare Funktion CompositionLocalProvider bindet Werte an CompositionLocal Instanzen für die angegebene Hierarchie. Verwenden Sie die provides Infix-Funktion, um einen neuen Wert für CompositionLocal bereitzustellen. Dabei wird ein CompositionLocal-Schlüssel einem value zugeordnet:

// 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 verwenden

CompositionLocal.current gibt den Wert zurück, der vom nächstgelegenen CompositionLocalProvider bereitgestellt wird, der einen Wert für 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

CompositionLocal ist für einige Anwendungsfälle möglicherweise eine übermäßige Lösung. Wenn Ihr Anwendungsfall die im Abschnitt Entscheidung für oder gegen CompositionLocal angegebenen Kriterien nicht erfüllt, ist eine andere Lösung wahrscheinlich besser geeignet.

Explizite Parameter übergeben

Es ist eine gute Gewohnheit, die Abhängigkeiten einer zusammensetzbaren Funktion explizit anzugeben. Wir empfehlen, zusammensetzbaren Funktionen nur das zu übergeben, was sie benötigen. Um die Entkopplung und Wiederverwendung von zusammensetzbaren Funktionen zu fördern, sollte jede zusammensetzbare Funktion so wenige 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, unnötige Abhängigkeiten an eine zusammensetzbare Funktion zu übergeben, ist die Verwendung der Inversion of Control. Anstatt dass der Nachfolger eine Abhängigkeit aufnimmt, um eine bestimmte Logik auszuführen, übernimmt das der Vorgänger.

Sehen Sie sich das folgende Beispiel an, in dem ein Nachfolger die Anfrage zum Laden von Daten auslösen muss:

@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 viele Aufgaben haben. Außerdem wird MyDescendant durch die Übergabe von MyViewModel als Abhängigkeit weniger wiederverwendbar, da sie jetzt miteinander gekoppelt sind. Sehen Sie sich die Alternative an, bei der die Abhängigkeit nicht an das Nachfolgerelement übergeben wird und die Prinzipien der Inversion of Control verwendet werden, sodass der Vorgänger für die Ausführung der Logik verantwortlich ist:

@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. Zusammensetzbare Vorgängerfunktionen werden in der Regel komplexer, um flexiblere zusammensetzbare Funktionen auf niedrigerer Ebene zu ermöglichen.

Ebenso können @Composable-Inhaltslambdas auf dieselbe Weise 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()
    }
}