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 eigene CompositionLocal erstellen und ob ein CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist.

Jetzt neu: CompositionLocal

Normalerweise fließen Daten in Compose abwärts durch den UI-Baum als Parameter für jede zusammensetzbare Funktion. Dadurch werden die Abhängigkeiten einer Composable explizit. Bei Daten, die sehr häufig und weit verbreitet verwendet werden, z. B. Farben oder Schriftarten, kann dies jedoch umständlich 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 Baumumfang erstellen können, die als implizite Möglichkeit verwendet werden können, Daten durch den UI-Baum fließen zu lassen.

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

CompositionLocal wird intern vom Material-Design verwendet. MaterialTheme ist ein Objekt, das drei CompositionLocal-Instanzen bereitstellt: colorScheme, typography und shapes. Sie können später in jedem untergeordneten Teil der Komposition abgerufen werden. Das sind 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 Komposition beschränkt. So können Sie auf verschiedenen Ebenen des Baums unterschiedliche Werte angeben. Der current-Wert eines CompositionLocal entspricht dem nächstgelegenen Wert, der von einem übergeordneten Element in diesem Teil der Komposition bereitgestellt wird.

Wenn Sie einen neuen Wert für ein CompositionLocal bereitstellen möchten, verwenden Sie die CompositionLocalProvider-Funktion und ihre Infix-Funktion provides, die einen CompositionLocal-Schlüssel einem value zuordnet. Die content-Lambdafunktion des CompositionLocalProvider erhält den bereitgestellten Wert, wenn auf die current-Eigenschaft des CompositionLocal zugegriffen wird. Wenn ein neuer Wert angegeben wird, führt Compose eine Neukomposition von Teilen der Komposition durch, in denen CompositionLocal gelesen wird.

Ein Beispiel dafür ist die LocalContentColor CompositionLocal, die die bevorzugte Inhaltsfarbe für Text und Symbole enthält, damit sie sich vom aktuellen Hintergrund abhebt. Im folgenden Beispiel wird CompositionLocalProvider verwendet, um verschiedene 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")
}

Abbildung 1: Vorschau der zusammensetzbaren Funktion CompositionLocalExample.

Im letzten Beispiel wurden die CompositionLocal-Instanzen intern von Material-Composables verwendet. Wenn Sie auf den aktuellen Wert eines CompositionLocal zugreifen möchten, verwenden Sie das Attribut current. Im folgenden Beispiel wird der aktuelle Context-Wert von 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 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 Zwischenschichten der Implementierung nicht wissen sollten, dass er vorhanden ist, da dies die Nützlichkeit der zusammensetzbaren Funktion einschränken würde. Für die Abfrage von Android-Berechtigungen wird beispielsweise im Hintergrund ein CompositionLocal verwendet. Ein Media Picker-Composable kann neue Funktionen hinzufügen, um auf berechtigungsgeschützte Inhalte auf dem Gerät zuzugreifen, ohne die API zu ändern. Aufrufer des Media Pickers müssen sich nicht des zusätzlichen Kontexts bewusst sein, 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 es, das Verhalten einer Composable-Funktion nachzuvollziehen. Da sie implizite Abhängigkeiten erstellen, müssen Aufrufer von Composables, die sie verwenden, dafür sorgen, dass ein Wert für jedes CompositionLocal angegeben wird.

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 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 angegeben wurde. Tools wie Verwendungen suchen in der IDE oder der Compose-Layoutinspektor liefern 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:

Ein CompositionLocal sollte einen guten Standardwert haben. Wenn es keinen Standardwert gibt, müssen Sie dafür sorgen, dass es für Entwickler äußerst schwierig ist, in eine Situation zu geraten, in der kein Wert für CompositionLocal angegeben wird. Wenn Sie keinen Standardwert angeben, kann das zu Problemen und Frustration führen, wenn Sie Tests erstellen oder eine Komponente in der Vorschau ansehen, die CompositionLocal verwendet. In diesem Fall muss der Wert immer explizit angegeben werden.

Vermeiden Sie CompositionLocal für Konzepte, die nicht als baum- oder unterhierarchiebezogen gelten. Ein CompositionLocal ist sinnvoll, wenn es 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 ein CompositionLocal erstellen.

Ein Beispiel für eine schlechte Vorgehensweise ist das Erstellen eines CompositionLocal, das das ViewModel eines bestimmten Bildschirms enthält, sodass alle Composables auf diesem Bildschirm einen Verweis auf das ViewModel erhalten können, um eine bestimmte Logik auszuführen. Das ist keine gute Vorgehensweise, da nicht alle Composables unter einem bestimmten UI-Baum über ein ViewModel informiert sein müssen. Es empfiehlt sich, Composables nur die Informationen zu übergeben, die sie benötigen, und dem Muster zu folgen, dass der Status nach unten und Ereignisse nach oben fließen. So werden Ihre Composables wiederverwendbarer und lassen sich leichter testen.

CompositionLocal erstellen

Es gibt zwei APIs zum Erstellen eines CompositionLocal:

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

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf werden Lesevorgänge eines staticCompositionLocalOf nicht von Compose erfasst. Wenn Sie den Wert ändern, wird das gesamte content-Lambda, in dem CompositionLocal bereitgestellt wird, neu zusammengesetzt. Das gilt nicht nur für die Stellen, an denen der current-Wert in der Komposition gelesen wird.

Wenn sich der für CompositionLocal angegebene Wert sehr wahrscheinlich nicht oder nie ändert, verwenden Sie staticCompositionLocalOf, um die Leistung zu verbessern.

Das Designsystem einer App kann beispielsweise vorgeben, wie Composables mithilfe eines Schattens für die UI-Komponente erhöht werden. Da sich die verschiedenen Erhebungen für die App im gesamten UI-Baum fortsetzen sollen, verwenden wir ein CompositionLocal. Da der Wert CompositionLocal bedingt auf Grundlage des Systemdesigns 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 einen neuen Wert für eine CompositionLocal bereitstellen möchten, verwenden Sie die Infix-Funktion provides, die 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 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

Ein CompositionLocal ist für einige Anwendungsfälle möglicherweise eine überzogene Lösung. Wenn Ihr Anwendungsfall nicht den Kriterien im Abschnitt Entscheiden, ob CompositionLocal verwendet werden soll entspricht, ist wahrscheinlich eine andere Lösung besser geeignet.

Explizite Parameter übergeben

Es ist eine gute Angewohnheit, die Abhängigkeiten von zusammensetzbaren Funktionen explizit anzugeben. Wir empfehlen, Composable-Funktionen nur das zu übergeben, was sie benötigen. 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, unnötige Abhängigkeiten an ein Composable zu übergeben, ist die Inversion of Control. Anstatt dass das untergeordnete Element eine Abhängigkeit für die Ausführung einer bestimmten 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 haben. Außerdem wird MyDescendant weniger wiederverwendbar, wenn MyViewModel als Abhängigkeit übergeben wird, da die beiden jetzt gekoppelt sind. Sehen Sie sich die Alternative an, bei der die Abhängigkeit nicht an den Nachfolger übergeben wird und die Inversion-of-Control-Prinzipien verwendet werden, wodurch der Vorfahre 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 übergeordneten Elementen entkoppelt. Ancestor-Composables werden in der Regel komplexer, um flexiblere Low-Level-Composables zu ermöglichen.

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