Daten auf lokaler Ebene mit CompositionLocal

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

Jetzt neu: CompositionLocal

In Compose fließen in der Regel Daten nach unten durch den UI-Baum als Parameter für jede zusammensetzbare Funktion. Dadurch werden die Abhängigkeiten einer zusammensetzbaren Funktion explizit. Bei Daten, die sehr häufig und weit verbreitet sind, wie Farben oder Schriftstile, kann dies jedoch 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 Composer CompositionLocal. Damit können benannte Objekte auf Baumebene erstellt werden, sodass Daten implizit durch die Struktur der Benutzeroberfläche fließen.

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

Im Hintergrund wird CompositionLocal verwendet. MaterialTheme ist ein Objekt mit drei CompositionLocal-Instanzen – Farben, Typografie und Formen –, die Sie später in jedem nachfolgenden Teil der Komposition abrufen können. Insbesondere 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 auf einen Teil der Zusammensetzung beschränkt, sodass Sie auf verschiedenen Ebenen der Baumstruktur unterschiedliche Werte angeben können. Der current-Wert einer CompositionLocal entspricht dem Wert, der am nächsten von einem Ancestor in diesem Teil der Zusammensetzung 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. Das Lambda content von CompositionLocalProvider erhält den angegebenen Wert, wenn auf das Attribut current von CompositionLocal zugegriffen wird. Wenn ein neuer Wert angegeben wird, setzt „Compose“ Teile der Komposition neu zusammen, die CompositionLocal lesen.

Ein Beispiel: Die CompositionLocal-Datei LocalContentAlpha enthält den bevorzugten Alpha-Bereich für Inhalte, der für Text und Symbole verwendet wird, um verschiedene Teile der UI zu betonen oder abzuschwächen. Im folgenden Beispiel werden mit CompositionLocalProvider verschiedene Werte für verschiedene Teile der Komposition angegeben.

@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 zusammensetzbaren Materialien verwendet. Um auf den aktuellen Wert einer CompositionLocal zuzugreifen, verwenden Sie deren Attribut 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)
}

Eigenes 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. Eine zusammensetzbare Media-Auswahl kann neue Funktionen für den Zugriff auf berechtigungsgeschützte Inhalte auf dem Gerät bieten, ohne die API zu ändern. Außerdem müssen die Aufrufer der Media-Auswahl über diesen zusätzlichen Kontext, der aus der Umgebung verwendet wird, informiert sein.

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

Mit CompositionLocal lässt sich das Verhalten einer zusammensetzbaren Funktion erschweren. 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 Datenquelle für diese Abhängigkeit, da sie an jedem Teil der Komposition verändert werden 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 Find uses (Nutzungen finden) in der IDE oder Compose Layout Inspector liefern genügend Informationen zur Behebung dieses Problems.

Entscheiden, ob CompositionLocal verwendet werden soll

Es gibt bestimmte Bedingungen, die CompositionLocal zu einer guten Lösung für Ihren Anwendungsfall machen können:

Ein CompositionLocal sollte einen guten Standardwert haben. Wenn kein Standardwert vorhanden ist, müssen Sie darauf achten, dass es für einen Entwickler sehr schwierig ist, in eine Situation zu geraten, in der kein Wert für CompositionLocal angegeben ist. Wenn Sie keinen Standardwert angeben, kann dies zu Problemen und Frustration beim Erstellen von Tests oder der Vorschau einer zusammensetzbaren Funktion führen, in der CompositionLocal verwendet wird. Dann ist die explizite Angabe immer erforderlich.

Vermeiden Sie CompositionLocal für Konzepte, die nicht als Baum- oder Unterhierarchie gelten. Ein CompositionLocal ist sinnvoll, wenn es potenziell von jedem Nachfolgerelement und nicht von einigen wenigen verwendet werden kann.

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 das ViewModel eines bestimmten Bildschirms enthält, sodass alle zusammensetzbaren Funktionen in diesem Bildschirm einen Verweis auf das ViewModel abrufen können, um eine Logik auszuführen. Dies ist nicht empfehlenswert, da nicht alle zusammensetzbaren Funktionen unter einem bestimmten UI-Baum Informationen zu einem ViewModel benötigen. 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. Mit diesem Ansatz werden die zusammensetzbaren Funktionen wiederverwendbar und lassen sich leichter testen.

CompositionLocal wird erstellt

Es gibt zwei APIs zum Erstellen einer CompositionLocal:

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

  • staticCompositionLocalOf: Im Gegensatz zu compositionLocalOf werden Lesevorgänge von staticCompositionLocalOf nicht durch „Compose“ verfolgt. 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 bereitgestellte Wert sehr unwahrscheinlich oder nie ändern wird, verwenden Sie staticCompositionLocalOf, um Leistungsvorteile zu erhalten.

Beispielsweise kann das Designsystem einer App anders formuliert sein, um zusammensetzbare Funktionen mit einem Schatten für die UI-Komponente zu optimieren. Da die verschiedenen Höhen für die Anwendung im gesamten UI-Baum übernommen werden sollen, verwenden wir eine CompositionLocal. Da der Wert CompositionLocal bedingt anhand 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 CompositionLocal angeben

Die zusammensetzbare Funktion CompositionLocalProvider bindet Werte an CompositionLocal-Instanzen für die angegebene Hierarchie. Um einen neuen Wert für ein CompositionLocal-Objekt anzugeben, 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 des nächstgelegenen CompositionLocalProvider zurück, der einen Wert für CompositionLocal liefert:

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

Mögliche Alternativen

CompositionLocal kann für manche 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 Gewohnheit, die Abhängigkeiten der zusammensetzbaren Funktion explizit anzugeben. Wir empfehlen, zusammensetzbare Funktionen nur zu übergeben, was sie benötigen. Um die Entkopplung und Wiederverwendung der 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 Weitergabe unnötiger Abhängigkeiten an eine zusammensetzbare Funktion zu vermeiden, ist die Kontrollumkehr. Anstatt eine Abhängigkeit zu übernehmen, um eine Logik auszuführen, übernimmt das übergeordnete Element dies stattdessen.

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

@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 viel Verantwortung übernehmen. Außerdem wird durch die Übergabe von MyViewModel als Abhängigkeit die Wiederverwendbarkeit von MyDescendant verringert, da die beiden jetzt gekoppelt sind. Betrachten Sie die Alternative, die die Abhängigkeit nicht an das untergeordnete Element übergibt und die Inversion der Steuerungsprinzipien verwendet, durch die 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 Vorgängern entkoppelt wird. Zusammensetzbare Ancestors werden tendenziell komplexer, da sie flexiblere zusammensetzbare Funktionen auf unterer Ebene haben.

In ähnlicher Weise können @Composable-Lambdas für Inhalte auf die gleiche 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()
    }
}