Daten auf lokaler Ebene mit CompositionLocal

CompositionLocal ist ein Tool, mit dem Daten implizit über die Komposition weitergegeben werden. Auf dieser Seite erfahren Sie mehr darüber, was eine CompositionLocal ist, wie Sie Ihre eigene CompositionLocal erstellen und ob eine CompositionLocal eine gute Lösung für Ihren Anwendungsfall ist.

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. Das kann jedoch bei Daten, die sehr häufig und weit verbreitet sind, wie Farben oder Schriftarten, 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 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.

CompositionLocal wird vom Material-Design verwendet. MaterialTheme ist ein Objekt mit drei CompositionLocal-Instanzen: colorScheme, typography und shapes. Sie können sie später in einem beliebigen untergeordneten Teil der Komposition abrufen. Insbesondere 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 Komposition beschränkt, sodass Sie auf verschiedenen Ebenen des Baums unterschiedliche Werte angeben können. Der current-Wert eines CompositionLocal entspricht dem Wert, der dem Element am nächsten ist, der von einem übergeordneten Element in diesem Teil der Komposition bereitgestellt wird.

Um einen neuen Wert für eine CompositionLocal bereitzustellen, verwende die CompositionLocalProvider und die zugehörige provides-Infix-Funktion, die einen CompositionLocal-Schlüssel mit einer value verknüpft. Die content-Lambda-Funktion der CompositionLocalProvider erhält den angegebenen Wert, wenn auf die current-Property der CompositionLocal zugegriffen wird. Wenn ein neuer Wert angegeben wird, setzt „Compose“ Teile der Komposition neu zusammen, die CompositionLocal lesen.

Beispielsweise enthält LocalContentColor CompositionLocal die bevorzugte Inhaltsfarbe, die für Text und Symbole verwendet wird, 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 des CompositionLocalExample-Kompositionselements

Im letzten Beispiel wurden die CompositionLocal-Instanzen intern von Material-Kompositen verwendet. Wenn Sie auf den aktuellen Wert eines CompositionLocal zugreifen möchten, verwenden Sie die Property current. Im folgenden Beispiel wird zur Formatierung des Textes der aktuelle Wert Context von LocalContext CompositionLocal verwendet, der häufig in Android-Apps verwendet wird:

@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 Weitergeben von Daten durch die Komposition.

Ein weiteres wichtiges Signal für die Verwendung von CompositionLocal ist, dass der Parameter sich kreuzt und die Zwischenschichten der Implementierung nicht wissen sollten, dass sie vorhanden sind, da die Berücksichtigung dieser Zwischenebenen den Nutzen der zusammensetzbaren Funktion einschränken würde. Zum Beispiel ermöglicht die Abfrage von Android-Berechtigungen eine CompositionLocal im Hintergrund. Mit einer zusammensetzbaren Medienauswahl können neue Funktionen zum Zugriff auf berechtigungsbasierte Inhalte auf dem Gerät hinzugefügt werden, ohne die API zu ändern und ohne dass die Aufrufer der Medienauswahl über diesen zusätzlichen Kontext aus der Umgebung informiert werden 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 hat:

CompositionLocal erschwert es, das Verhalten eines komponierbaren Elements zu verstehen. Da implizite Abhängigkeiten erstellt werden, müssen die Aufrufer von zusammensetzbaren Funktionen, die diese Funktionen verwenden, dafür sorgen, dass für jede CompositionLocal ein Wert 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 das Fehler in der Anwendung bei Auftreten eines Problems schwieriger sein, da Sie in der Zusammensetzung nach oben gehen müssen, um zu sehen, wo der Wert current 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 festgelegt ist, 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 ist. Wenn Sie keinen Standardwert angeben, kann das beim Erstellen von Tests oder bei der Vorschau eines Composeables, in dem dieser Wert verwendet wird, zu Problemen führen. CompositionLocal muss dann immer 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 schlechte Praxis ist das Erstellen einer CompositionLocal, die die ViewModel eines bestimmten Bildschirms enthält, damit alle Composeables auf diesem Bildschirm einen Verweis auf die ViewModel erhalten können, 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, an zusammensetzbare Funktionen nur die Informationen zu übergeben, die sie benötigen. Dabei gilt folgendes Muster: Zustand fließen nach unten und Ereignisse fließen nach oben. So lassen sich Ihre Composables leichter wiederverwenden und testen.

CompositionLocal erstellen

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

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

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf werden Lesevorgänge von staticCompositionLocalOf von Compose nicht erfasst. Wenn Sie den Wert ändern, wird die gesamte Lambda-Funktion content, in der das CompositionLocal angegeben ist, neu zusammengesetzt, nicht nur die Stellen, an denen der Wert current in der Zusammensetzung gelesen wird.

Wenn sich der für CompositionLocal angegebene Wert höchstwahrscheinlich nicht ändert oder sich nie ändert, verwenden Sie staticCompositionLocalOf, um Leistungsvorteile zu erzielen.

Das Designsystem einer App kann beispielsweise eine bestimmte Meinung dazu haben, wie Kompositionen mithilfe eines Schattens für die UI-Komponente hervorgehoben werden. Da die verschiedenen Ebenen der App im gesamten UI-Baum übernommen werden sollen, verwenden wir eine CompositionLocal. Da der Wert für CompositionLocal bedingt vom Systemthema 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. Wenn Sie einer CompositionLocal einen neuen Wert zuweisen möchten, verwenden Sie die Infixfunktion provides, um einen CompositionLocal-Schlüssel einem value zuzuweisen:

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

Alternativen in Betracht ziehen

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

Explizite Parameter übergeben

Es ist eine gute Angewohnheit, die Abhängigkeiten von Composeable-Objekten explizit anzugeben. Wir empfehlen, zusammensetzbare Funktionen nur zu übergeben, was sie benötigen. Um die Entkopplung und Wiederverwendung von Composeable-Elementen zu fördern, sollte jedes Composeable-Element möglichst wenig Informationen 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 Logik übernimmt, übernimmt das übergeordnete Element dies stattdessen.

Im folgenden Beispiel muss ein untergeordneter Knoten 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 hat MyDescendant möglicherweise eine große Verantwortung. Außerdem ist MyDescendant dann weniger wiederverwendbar, da sie jetzt miteinander verknüpft sind.MyViewModel Betrachten Sie die Alternative, bei der die Abhängigkeit nicht an den untergeordneten Knoten übergeben wird und die Prinzipien der Inversion of Control verwendet, die den übergeordneten Knoten 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 das untergeordnete Element von seinen unmittelbaren Vorfahren getrennt wird. Übergeordnete Composeables werden in der Regel komplexer, um flexiblere Composeables auf niedrigerer Ebene zu ermöglichen.

Auf ähnliche Weise können @Composable-Inhaltslamben 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()
    }
}