Daten auf lokaler Ebene mit CompositionLocal

CompositionLocal ist ein Tool zum impliziten Übergeben von Daten durch die Komposition. Auf dieser Seite erfahren Sie im Detail, was ein CompositionLocal ist, wie Sie Ihr eigenes CompositionLocal erstellen und ob ein CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist.

Jetzt neu: CompositionLocal

In der Regel fließen in „Compose“ Daten durch den UI-Baum als Parameter nach unten zu den einzelnen zusammensetzbaren Funktionen. Dadurch werden die Abhängigkeiten einer zusammensetzbaren Funktion explizit. Dies kann jedoch bei Daten, die sehr häufig und häufig verwendet werden, z. B. Farben oder Schriftstile, umständlich sein. Hier ein 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 zusammensetzbaren Funktionen übergeben werden müssen, bietet Compose CompositionLocal an. Damit können Sie baumbasierte benannte Objekte erstellen, die implizit für den Datenfluss durch die UI-Struktur verwendet werden können.

CompositionLocal-Elemente werden normalerweise mit einem Wert in einem bestimmten Knoten des UI-Baums angegeben. Dieser Wert kann von seinen zusammensetzbaren Nachfolgern verwendet werden, ohne dass CompositionLocal als Parameter in der zusammensetzbaren Funktion deklariert wird.

CompositionLocal wird beim Material Theme im Hintergrund verwendet. MaterialTheme ist ein Objekt, das drei CompositionLocal-Instanzen (Farben, Typografie und Formen) bereitstellt, mit denen du diese später in jedem nachfolgenden Teil der Komposition abrufen kannst. Im Einzelnen sind dies die Attribute LocalColors, LocalShapes und LocalTypography, auf die Sie über die Attribute MaterialTheme, colors, shapes und typography zugreifen können.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.primary
    )
}

Eine CompositionLocal-Instanz ist einem Teil der Zusammensetzung zugeordnet, sodass Sie auf verschiedenen Baumebenen unterschiedliche Werte angeben können. Der current-Wert einer CompositionLocal entspricht dem nächstgelegenen Wert, der von einem Ancestor in diesem Teil der Komposition bereitgestellt wird.

Wenn Sie einem CompositionLocal einen neuen Wert zuweisen möchten, verwenden Sie den CompositionLocalProvider und die zugehörige Infix-Funktion provides, die einen CompositionLocal-Schlüssel mit einem value verknüpft. Das Lambda content des CompositionLocalProvider erhält den angegebenen Wert, wenn auf das Attribut current von CompositionLocal zugegriffen wird. Wenn ein neuer Wert angegeben wird, setzt die Funktion „Compose“ Teile der Zusammensetzung neu zusammen, die die CompositionLocal lesen.

Beispielsweise enthält der CompositionLocal LocalContentAlpha den bevorzugten Alpha-Inhalt für Text und Ikonografie, um verschiedene Teile der Benutzeroberfläche hervorzuheben oder abzuschwächen. Im folgenden Beispiel wird CompositionLocalProvider verwendet, um verschiedene Werte für verschiedene Teile der Komposition bereitzustellen.

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

Abbildung 1: Vorschau der zusammensetzbaren Funktion CompositionLocalExample.

In allen obigen Beispielen wurden die CompositionLocal-Instanzen intern von Material-Zusammensetzbaren verwendet. Verwenden Sie das Attribut current, um auf den aktuellen Wert einer CompositionLocal zuzugreifen. Im folgenden Beispiel wird der aktuelle Context-Wert des LocalContext-CompositionLocal, der häufig in Android-Apps verwendet wird, zur Formatierung des Textes 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 über die Zusammensetzung.

Ein weiteres wichtiges Signal für die Verwendung von CompositionLocal besteht darin, dass der Parameter Querschnittsschichten ist und Zwischenschichten der Implementierung nicht erkennen sollten, dass sie existiert, da die Kenntnis dieser Zwischenschichten den Nutzen der zusammensetzbaren Funktion einschränken würde. Beispielsweise ermöglicht ein CompositionLocal-Objekt das Abfragen von Android-Berechtigungen. Eine zusammensetzbare Medienauswahl kann neue Funktionen für den Zugriff auf berechtigungsgeschützte Inhalte auf dem Gerät hinzufügen, ohne die API zu ändern. Außerdem müssen Aufrufer der Medienauswahl über diesen zusätzlichen Kontext aus der Umgebung informiert sein.

CompositionLocal ist jedoch nicht immer die beste Lösung. Wir raten von der übermäßigen Verwendung von CompositionLocal ab, da dies einige Nachteile mit sich bringt:

Mit CompositionLocal ist das Verhalten einer zusammensetzbaren Funktion schwerer verständlich. Beim Erstellen impliziter Abhängigkeiten müssen Aufrufer von zusammensetzbaren Funktionen, die diese verwenden, dafür sorgen, dass ein Wert für jede CompositionLocal erfüllt ist.

Außerdem gibt es für diese Abhängigkeit möglicherweise keine eindeutige Quelle der Wahrheit, da sie in jedem Teil der Zusammensetzung mutieren kann. Daher kann das Debuggen der Anwendung bei einem Problem schwieriger sein, da Sie in der Zusammensetzung nach oben gehen müssen, um zu sehen, wo der Wert current angegeben wurde. Tools wie Nutzungen suchen in der IDE oder der Compose Layout Inspector bieten genügend Informationen, um dieses Problem zu beheben.

Entscheiden, ob CompositionLocal verwendet werden soll

Wenn CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist, gelten bestimmte Bedingungen:

CompositionLocal sollte einen guten Standardwert haben. Falls kein Standardwert vorhanden ist, muss sichergestellt werden, dass es für einen Entwickler sehr schwierig ist, in eine Situation zu kommen, in der kein Wert für CompositionLocal angegeben ist. Wenn kein Standardwert angegeben wird, kann es beim Erstellen von Tests oder bei der Vorschau einer zusammensetzbaren Funktion, die diesen CompositionLocal verwendet, zu Problemen und Frustration kommen.

Vermeiden Sie CompositionLocal für Konzepte, die nicht auf Baum- oder Unterhierarchieebene ausgelegt sind. Ein CompositionLocal ist sinnvoll, wenn sie potenziell von einem Nachfolgerelement und nicht von einigen wenigen von ihnen verwendet werden kann.

Wenn Ihr Anwendungsfall diese Anforderungen nicht erfüllt, sehen Sie sich den Abschnitt Alternativen an, bevor Sie eine CompositionLocal erstellen.

Ein Beispiel für eine schlechte Praxis ist das Erstellen eines CompositionLocal-Objekts, das den ViewModel eines bestimmten Bildschirms enthält, sodass alle zusammensetzbaren Funktionen in diesem Bildschirm einen Verweis auf die ViewModel erhalten können, um eine Logik auszuführen. Dies ist nicht empfehlenswert, da nicht alle zusammensetzbaren Funktionen unterhalb einer bestimmten UI-Struktur über eine ViewModel informiert werden müssen. Es empfiehlt sich, nur die Informationen an zusammensetzbare Funktionen weiterzugeben, die dem Muster folgen, das den Zustand nach unten und die Ereignisse nach oben. Dieser Ansatz macht Ihre zusammensetzbaren Funktionen wiederverwendbarer und einfacher zu testen.

CompositionLocal wird erstellt

CompositionLocal kann mit zwei APIs erstellt werden:

  • compositionLocalOf: Wenn Sie den bei der Neuzusammensetzung angegebenen Wert ändern, wird nur der Inhalt ungültig, der seinen current-Wert liest.

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf werden Lesevorgänge eines staticCompositionLocalOf-Objekts nicht von der Funktion "Compose" erfasst. Durch das Ändern des Werts wird die gesamte Lambda-Funktion content, an der CompositionLocal angegeben ist, und nicht nur die Stellen, an denen der current-Wert in der Zusammensetzung gelesen wird, neu zusammengesetzt.

Wenn es höchst unwahrscheinlich ist, dass sich der für CompositionLocal angegebene Wert ändert oder nie ändert, verwenden Sie staticCompositionLocalOf, um Leistungsvorteile zu erzielen.

Beispielsweise könnte das Designsystem einer App so strukturiert sein, wie zusammensetzbare Funktionen durch einen Schatten für die UI-Komponente erhöht werden. Da die verschiedenen Höhen für die Anwendung im gesamten UI-Baum verteilt werden sollen, verwenden wir CompositionLocal. Da der Wert CompositionLocal basierend auf dem Systemthema 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

Die zusammensetzbare Funktion CompositionLocalProvider bindet Werte an CompositionLocal-Instanzen für die angegebene Hierarchie. Wenn Sie einem CompositionLocal einen neuen Wert zuweisen möchten, verwenden Sie die Infix-Funktion provides, die einen CompositionLocal-Schlüssel so mit einer 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, das einen Wert für diese CompositionLocal bereitstellt:

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

Alternativen

Ein CompositionLocal kann für einige Anwendungsfälle eine übermäßige Lösung sein. Wenn Ihr Anwendungsfall die im Abschnitt Entscheiden, ob CompositionLocal zu verwenden angegebenen Kriterien nicht erfüllt, ist möglicherweise eine andere Lösung für Ihren Anwendungsfall möglicherweise besser geeignet.

Explizite Parameter übergeben

Es ist eine gute Gewohnheit, die Abhängigkeiten von zusammensetzbaren Funktionen explizit anzugeben. Wir empfehlen, zusammensetzbare Funktionen nur zu übergeben, was sie benötigen. Um das Entkoppeln und Wiederverwenden von zusammensetzbaren Funktionen zu fördern, sollte jede zusammensetzbare Funktion 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
}

Umkehrung der Kontrolle

Eine weitere Möglichkeit, die Übergabe unnötiger Abhängigkeiten an eine zusammensetzbare Funktion zu vermeiden, ist die Umkehrung der Kontrolle. Anstatt dass der Nachfolger eine Abhängigkeit übernimmt, um eine Logik auszuführen, führt dies stattdessen das übergeordnete Element aus.

Im folgenden Beispiel muss ein Nachfolger die Anfrage zum Laden einiger 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 trägt MyDescendant möglicherweise eine hohe Verantwortung. Außerdem macht das Übergeben von MyViewModel als Abhängigkeit die Wiederverwendbarkeit von MyDescendant geringer, da die beiden jetzt miteinander gekoppelt sind. Betrachten Sie die Alternative, die die Abhängigkeit nicht an das untergeordnete Element übergibt und eine Umkehrung der Steuerungsprinzipien verwendet, wodurch der Ancestor 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 eignet sich für einige Anwendungsfälle besser, da das untergeordnete Element von seinen unmittelbaren Ancestors entkoppelt wird. Ancestors werden in der Regel komplexer, da sie flexiblere untergeordnete Komponenten bieten.

In ähnlicher Weise können @Composable-Inhalts-Lambdas auf dieselbe Weise verwendet werden, um dieselben Vorteile zu erhalten:

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

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