Daten auf lokaler Ebene mit CompositionLocal

CompositionLocal ist ein Tool, mit dem Daten implizit durch die Komposition übergeben werden. Auf dieser Seite werden Sie mehr über CompositionLocal erfahren, wie Sie eigene erstellen CompositionLocal. Wenn CompositionLocal eine gute Lösung ist, Ihren Anwendungsfall.

Jetzt neu: CompositionLocal

Normalerweise fließen Daten in Compose als Parameter durch den UI-Baum hinunter zu jeder zusammensetzbaren Funktion. Dadurch werden die Abhängigkeiten eines Composeables explizit. Dies kann jedoch umständlich für Daten sein, die sehr häufig und weit verbreitet, wie Farben oder Schriftstile. Weitere Informationen: Beispiel:

@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 Compose-Elemente übergeben werden müssen, bietet Compose CompositionLocal, mit dem Sie benannte Objekte auf Baumebene erstellen können, die als implizite Methode zum Durchleiten von Daten durch den UI-Baum verwendet werden können.

CompositionLocal-Elemente werden in der Regel in einem bestimmten Knoten des UI-Baums mit einem Wert versehen. Dieser Wert kann von den übergeordneten Funktionen verwendet werden, ohne dass CompositionLocal als Parameter in der zusammensetzbaren Funktion deklariert werden muss.

Im Hintergrund wird CompositionLocal verwendet. MaterialTheme ist ein Objekt, das drei CompositionLocal-Instanzen bereitstellt: colorScheme, typography und shapes, sodass Sie sie später in einem beliebigen als Teil der Komposition. Das sind die Attribute LocalColorScheme, LocalShapes und LocalTypography, auf die Sie über die Attribute MaterialThemecolorScheme, 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 Komposition beschränkt, sodass Sie auf verschiedenen Ebenen des Baums unterschiedliche Werte angeben können. Der current-Wert eines CompositionLocal entspricht dem Wert, der von einem übergeordneten Element in diesem Teil der Komposition am nächsten liegt.

Wenn Sie einem CompositionLocal einen neuen Wert zuweisen möchten, verwenden Sie die Funktion CompositionLocalProvider und ihre Infixfunktion provides, die einem CompositionLocal-Schlüssel einen value-Wert zuordnet. Die content-Lambda-Funktion der CompositionLocalProvider erhält den angegebenen Wert, wenn auf die current-Property der CompositionLocal zugegriffen wird. Wenn ein wird ein neuer Wert angegeben, setzt „Compose“ die Teile der Komposition neu zusammen, CompositionLocal.

Als Beispiel enthält die CompositionLocal-Datei LocalContentColor die bevorzugte Inhaltsfarbe für Text und damit sie sich von der aktuellen Hintergrundfarbe abhebt. Im folgenden Beispiel wird CompositionLocalProvider verwendet, um verschiedene Werte für verschiedene Teile der Komposition anzugeben.

@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")
}

Abbildung 1. Vorschau der zusammensetzbaren Funktion CompositionLocalExample.

Im letzten Beispiel wurden die CompositionLocal-Instanzen intern verwendet durch „Material Componables“. So greifen Sie auf den aktuellen Wert einer CompositionLocal zu: Verwenden Sie die current Property. Im folgenden Beispiel wird der aktuelle Wert Context von LocalContext CompositionLocal, das häufig in Android-Apps verwendet wird, dient der Formatierung Text:

@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 Übergeben von Daten durch die Komposition implizit.

Ein weiteres wichtiges Signal für die Verwendung von CompositionLocal ist, wenn der Parameter schnittstellenübergreifend ist und Zwischenschichten der Implementierung nicht wissen sollten, dass er existiert, da dies die Nützlichkeit der Composeable-Funktion einschränken würde. Die Abfrage von Android-Berechtigungen wird beispielsweise von einem CompositionLocal unterstützt. Eine zusammensetzbare Media-Auswahl neue Funktionen für den Zugriff auf berechtigungsgeschützte Inhalte im ohne die API zu ändern und die Aufrufer der Media-Auswahl berücksichtigen Sie diesen zusätzlichen Kontext, der aus der Umgebung verwendet wird.

CompositionLocal ist jedoch nicht immer die beste Lösung. Mi. Vermeide eine zu häufige Verwendung von CompositionLocal, da dies einige Nachteile hat:

Mit CompositionLocal wird das Verhalten einer zusammensetzbaren Funktion schwerer nachvollziehbar. Als erstellen implizite Abhängigkeiten, die Aufrufer von zusammensetzbaren Funktionen, die diese verwenden, um sicherzustellen, dass ein Wert für jeden CompositionLocal erfüllt ist.

Außerdem gibt es möglicherweise keine eindeutige „Source of Truth“ für diese Abhängigkeit, da sie in jedem Teil der Komposition mutieren kann. Daher kann es schwieriger sein, die App zu debuggen, wenn ein Problem auftritt, da Sie sich durch die Komposition bewegen müssen, um zu sehen, wo der current-Wert angegeben wurde. Tools wie Verwendungsfälle finden in der IDE oder der Layout-Inspektor für Compose liefern ausreichend 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:

Ein CompositionLocal sollte einen guten Standardwert haben. Wenn kein Standardwert vorhanden ist müssen Sie garantieren, dass es für einen Entwickler außerordentlich schwierig ist, für den Fall, dass für CompositionLocal kein Wert angegeben ist. Wenn Sie keinen Standardwert angeben, kann dies zu Problemen und Frust beim Erstellen oder eine Vorschau einer zusammensetzbaren Funktion mit dieser CompositionLocal-Funktion ausführen, explizit angegeben werden.

Verwenden Sie CompositionLocal nicht für Konzepte, die nicht auf Baum- oder untergeordnete Hierarchieebene beschränkt sind. Eine CompositionLocal ist sinnvoll, wenn sie potenziell von allen untergeordneten Elementen 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 unzulässige Vorgehensweise ist das Erstellen einer CompositionLocal, die den ViewModel eines bestimmten Bildschirms, sodass alle zusammensetzbaren Funktionen in diesem Bildschirm einen Verweis auf das ViewModel abzurufen, um eine Logik auszuführen. Das ist nicht empfehlenswert, da nicht alle Composeables unter einem bestimmten UI-Baum eine ViewModel kennen müssen. Es hat sich bewährt, nur die Informationen dass sie dem Muster folgen müssen, dass der Zustand nach unten und Ereignisse nach oben fließen. Mit diesem Ansatz werden Ihre zusammensetzbaren Funktionen wiederverwendbar und leichter zu testen.

CompositionLocal wird erstellt

Es gibt zwei APIs, mit denen du eine CompositionLocal erstellen kannst:

  • compositionLocalOf: Wenn Sie den bei der Neuzusammensetzung angegebenen Wert ändern, wird nur der Inhalt ungültig, in dem der Wert current gelesen wird.

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf sind Lesevorgänge einer staticCompositionLocalOf nicht von Compose verfolgt. Wenn Sie den Wert ändern, wird die gesamte content-Lambda-Funktion, in der der CompositionLocal-Wert angegeben ist, neu zusammengesetzt. Das gilt nicht nur für die Stellen, an denen der current-Wert in der Komposition gelesen wird.

Es ist sehr unwahrscheinlich, dass sich der für CompositionLocal angegebene Wert ändert oder ändert sich nichts. Verwenden Sie staticCompositionLocalOf, um die Leistung zu verbessern.

Zum Beispiel könnte das Designsystem einer App hinsichtlich der Art und Weise, wie zusammensetzbare Funktionen werden mit einem Schatten für die UI-Komponente erhöht. Da die verschiedenen Höhen für die App im gesamten UI-Baum übernommen werden sollen, verwenden wir eine CompositionLocal. Da der Wert CompositionLocal bedingt 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 eine CompositionLocal angeben

Das CompositionLocalProvider-kompositum bindet Werte an CompositionLocal-Instanzen für die angegebene Hierarchie. Um einen neuen Wert für CompositionLocal festzulegen, verwenden Sie die Methode provides infix-Funktion, die wie folgt einen CompositionLocal-Schlüssel 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 des nächstgelegenen CompositionLocalProvider zurück, der einen Wert für diesen CompositionLocal angibt:

@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
    }
}

Mögliche Alternativen

Für einige Anwendungsfälle ist eine CompositionLocal möglicherweise eine übertriebene Lösung. Wenn Ihr Anwendungsfall nicht die Kriterien erfüllt, die unter Verwendung CompositionLocal angezeigt wird, könnte eine andere Lösung wahrscheinlich besser sein die für Ihren Anwendungsfall geeignet sind.

Explizite Parameter übergeben

Es ist eine gute Angewohnheit, die Abhängigkeiten von Composeable-Komponenten explizit anzugeben. Wir empfehlen, Sie zusammensetzbare Funktionen nur das übergeben, was sie benötigen. Um die Entkopplung zu fördern, und Wiederverwendung von zusammensetzbaren Funktionen ist, sollte jede zusammensetzbare Funktion die geringste Menge an Informationen möglich sind.

@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
}

Umkehrung der Kontrolle

Eine weitere Möglichkeit, die Übergabe unnötiger Abhängigkeiten an eine zusammensetzbare Funktion zu vermeiden, durch die Kontrollumkehr. Anstatt dass das untergeordnete Element eine Abhängigkeit für die Ausführung einer Logik übernimmt, übernimmt das übergeordnete Element dies stattdessen.

Im folgenden Beispiel wird gezeigt, wie ein Nachfolger die Anfrage auslösen muss, um Daten laden:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

Je nach Fall hat MyDescendant möglicherweise eine große Verantwortung. Außerdem ist MyDescendant dann weniger wiederverwendbar, da sie jetzt miteinander verknüpft sind. Überlegen Sie, welche Alternative die Abhängigkeit in die Nachfolgerelemente und wendet die Umkehrung der Kontrollprinzipien an, macht den Ancestor für die Ausführung der Logik verantwortlich:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

Dieser Ansatz eignet sich für einige Anwendungsfälle besser, da er die untergeordnetes Element von seinen unmittelbaren Vorgängern. Übergeordnete Composeables werden in der Regel komplexer, um flexiblere Composeables auf niedrigerer Ebene zu ermöglichen.

@Composable Content-Lambdas können ebenfalls auf die gleiche Weise verwendet werden, Vorteile:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}