Dane o zakresie lokalnym dzięki CompositionLocal

CompositionLocal to narzędzie do niejawnego przekazywania danych w dół kompozycji. Na tej stronie dowiesz się, czym jest CompositionLocal, jak utworzyć własny CompositionLocal i czy CompositionLocal jest dobrym rozwiązaniem w Twoim przypadku.

Wprowadzenie do CompositionLocal

Zwykle w Compose dane przepływają w dół drzewa interfejsu jako parametry każdej funkcji typu „composable”. Dzięki temu zależności funkcji kompozycyjnej są wyraźne. Może to być jednak uciążliwe w przypadku danych, które są bardzo często i powszechnie używane, takich jak kolory czy style czcionek. Przyjrzyj się temu przykładowi:

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

Aby nie trzeba było przekazywać kolorów jako jawnej zależności parametru do większości funkcji kompozycyjnych, Compose oferuje CompositionLocal, co pozwala tworzyć nazwane obiekty o zakresie drzewa, które mogą być używane jako niejawny sposób przepływu danych przez drzewo interfejsu.

Elementy CompositionLocal są zwykle dostarczane z wartością w określonym węźle drzewa interfejsu. Tej wartości mogą używać elementy podrzędne typu „composable” bez deklarowania CompositionLocal jako parametru w funkcji typu „composable”.

Kluczowe terminy: w tym przewodniku używamy terminów kompozycja, drzewo interfejsu i hierarchia interfejsu. Chociaż w innych przewodnikach mogą być używane zamiennie, mają różne znaczenia:

  • Kompozycja to zapis grafu wywołań funkcji typu „composable”.
  • Drzewo UI lub hierarchia UI to drzewo LayoutNode utworzone, aktualizowane i utrzymywane przez proces tworzenia kompozycji.

CompositionLocal to element, którego motyw Material używa w tle. MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typography i shapes, dzięki czemu można je później pobrać w dowolnej części podrzędnej kompozycji. Są to właściwości LocalColorScheme, LocalShapes i LocalTypography, do których można uzyskać dostęp za pomocą atrybutów MaterialTheme, colorScheme, shapes i typography.

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

Instancja CompositionLocal jest ograniczona do części funkcji kompozycyjnej, dzięki czemu możesz podawać różne wartości na różnych poziomach drzewa. Wartość current elementu CompositionLocal odpowiada najbliższej wartości podanej przez element nadrzędny w tej części kompozycji.

Aby podać nową wartość dla CompositionLocal, użyj CompositionLocalProvider i funkcji wrostkowej provides, która wiąże klucz CompositionLocal z value. Funkcja lambda content elementu CompositionLocalProvider otrzyma podaną wartość podczas uzyskiwania dostępu do właściwości current elementu CompositionLocal. Gdy zostanie podana nowa wartość, Compose ponownie skomponuje części kompozycji, które odczytują CompositionLocal.

Na przykład LocalContentColor CompositionLocal zawiera preferowany kolor treści używany w tekście i ikonach, aby zapewnić kontrast z bieżącym kolorem tła. W poniższym przykładzie CompositionLocalProvider służy do podawania różnych wartości dla różnych części kompozycji.

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

Podgląd funkcji kompozycyjnej CompositionLocalExample.
Rysunek 1. Podgląd funkcji kompozycyjnej CompositionLocalExample.

W ostatnim przykładzie instancje CompositionLocal były używane wewnętrznie przez komponenty Material. Aby uzyskać dostęp do bieżącej wartości CompositionLocal, użyj właściwości current. W tym przykładzie do sformatowania tekstu użyto bieżącej wartości Context parametru LocalContext CompositionLocal, który jest powszechnie używany w aplikacjach na Androida:

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

Utwórz własny artefakt: CompositionLocal

CompositionLocal to narzędzie do niejawnego przekazywania danych w dół kompozycji.

Innym ważnym sygnałem do użycia CompositionLocal jest sytuacja, gdy parametr jest przekrojowy, a pośrednie warstwy implementacji nie powinny wiedzieć o jego istnieniu, ponieważ ograniczyłoby to użyteczność funkcji kompozycyjnej. Na przykład zapytanie o uprawnienia Androida jest obsługiwane przez CompositionLocal. Komponent wyboru mediów może dodać nową funkcję dostępu do treści chronionych uprawnieniami na urządzeniu bez zmiany interfejsu API i bez konieczności informowania wywołujących wybór mediów o tym dodatkowym kontekście używanym w środowisku.

Jednak CompositionLocal nie zawsze jest najlepszym rozwiązaniem. Odradzamy nadmierne używanie CompositionLocal, ponieważ ma to pewne wady:

CompositionLocal utrudnia zrozumienie działania funkcji kompozycyjnej. Ponieważ tworzą one zależności niejawne, elementy wywołujące funkcje kompozycyjne, które ich używają, muszą upewnić się, że wartość każdego elementu CompositionLocal jest spełniona.

Ponadto może nie być jasnego źródła prawdy dla tej zależności, ponieważ może ona ulec zmianie w dowolnej części kompozycji. Dlatego debugowanie aplikacji w przypadku wystąpienia problemu może być trudniejsze, ponieważ musisz przejść w górę kompozycji, aby zobaczyć, gdzie została podana wartość current. Narzędzia takie jak Znajdź użycia w IDE lub Inspektor układu Compose dostarczają wystarczająco dużo informacji, aby rozwiązać ten problem.

Decydowanie o użyciu CompositionLocal

Istnieją pewne warunki, które mogą sprawić, że CompositionLocal będzie dobrym rozwiązaniem w Twoim przypadku:

CompositionLocal powinien mieć dobrą wartość domyślną. Jeśli nie ma wartości domyślnej, musisz zadbać o to, aby deweloperowi było bardzo trudno doprowadzić do sytuacji, w której nie podano wartości CompositionLocal. Niepodanie wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub podglądu funkcji kompozycyjnej, która używa tego elementu CompositionLocal, ponieważ zawsze będzie wymagać jego jawnego podania.

Unikaj używania CompositionLocal w przypadku pojęć, które nie są uważane za należące do zakresu drzewa lub podhierarchii. CompositionLocal ma sens, gdy może być używany przez dowolny element podrzędny, a nie tylko przez kilka z nich.

Jeśli Twój przypadek użycia nie spełnia tych wymagań, przed utworzeniem CompositionLocal zapoznaj się z sekcją Alternatywy do rozważenia.

Przykładem nieodpowiedniej metody jest utworzenie elementu CompositionLocal, który zawiera ViewModel konkretnego ekranu, aby wszystkie elementy kompozycyjne na tym ekranie mogły uzyskać odwołanie do ViewModel w celu wykonania określonej logiki. Jest to zła praktyka, ponieważ nie wszystkie funkcje kompozycyjne w danym drzewie interfejsu muszą wiedzieć o ViewModel. Sprawdzoną metodą jest przekazywanie do funkcji kompozycyjnych tylko tych informacji, których potrzebują, zgodnie ze wzorcem, że stan przepływa w dół, a zdarzenia w górę. Dzięki temu funkcje kompozycyjne będą bardziej wielokrotnego użytku i łatwiejsze do przetestowania.

Utwórz CompositionLocal

Do tworzenia CompositionLocal służą 2 interfejsy API:

  • compositionLocalOf: zmiana wartości podanej podczas rekompozycji unieważnia tylko treść, która odczytuje wartość current.

  • staticCompositionLocalOf: W przeciwieństwie do compositionLocalOf odczyty staticCompositionLocalOf nie są śledzone przez Compose. Zmiana wartości powoduje ponowne skomponowanie całej funkcji lambda content, w której podano CompositionLocal, a nie tylko miejsc, w których wartość current jest odczytywana w kompozycji.

Jeśli jest bardzo mało prawdopodobne, że wartość podana w parametrze CompositionLocal ulegnie zmianie, lub jeśli nigdy się nie zmieni, użyj staticCompositionLocalOf, aby zwiększyć wydajność.

Na przykład system projektowania aplikacji może być ukierunkowany na sposób, w jaki elementy kompozycyjne są podnoszone za pomocą cienia komponentu interfejsu. Ponieważ różne wysokości aplikacji powinny być propagowane w drzewie interfejsu, używamy CompositionLocal. Wartość CompositionLocal jest określana warunkowo na podstawie motywu systemu, dlatego używamy interfejsu API compositionLocalOf:

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

Podaj wartości dla: CompositionLocal

Funkcja kompozycyjna CompositionLocalProvider wiąże wartości z instancjami CompositionLocal w przypadku danej hierarchii. Aby podać nową wartość dla CompositionLocal, użyj funkcji wrostkowej provides, która wiąże klucz CompositionLocal z value w ten sposób:

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

Przetwarzanie: CompositionLocal

CompositionLocal.current zwraca wartość podaną przez najbliższy element CompositionLocalProvider, który dostarcza wartość do tego elementu CompositionLocal:

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

Alternatywy do rozważenia

W niektórych przypadkach użycia CompositionLocal może być zbyt rozbudowanym rozwiązaniem. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy używać CompositionLocal, prawdopodobnie lepiej sprawdzi się inne rozwiązanie.

Przekazywanie jawnych parametrów

Dobrym nawykiem jest wyraźne określanie zależności funkcji kompozycyjnej. Zalecamy przekazywanie do funkcji kompozycyjnych tylko tych danych, których potrzebują. Aby zachęcić do rozdzielania i ponownego używania funkcji kompozycyjnych, każda z nich powinna zawierać jak najmniej informacji.

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

Odwrócenie sterowania

Innym sposobem na uniknięcie przekazywania niepotrzebnych zależności do funkcji kompozycyjnej jest użycie odwrócenia sterowania. Zamiast elementu podrzędnego, który przyjmuje zależność w celu wykonania określonej logiki, robi to element nadrzędny.

Zobacz ten przykład, w którym element podrzędny musi wywołać żądanie wczytania danych:

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

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

W zależności od przypadku MyDescendant może mieć dużą odpowiedzialność. Poza tym przekazywanie MyViewModel jako zależności sprawia, że MyDescendant jest mniej przydatny do wielokrotnego użycia, ponieważ są teraz ze sobą powiązane. Rozważ alternatywne rozwiązanie, które nie przekazuje zależności do elementu podrzędnego i wykorzystuje zasady odwrócenia sterowania, dzięki którym element nadrzędny jest odpowiedzialny za wykonywanie logiki:

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

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

To podejście może być lepiej dostosowane do niektórych przypadków użycia, ponieważ oddziela element podrzędny od jego bezpośrednich elementów nadrzędnych. Funkcje kompozycyjne przodków stają się bardziej złożone, aby zapewnić większą elastyczność funkcji kompozycyjnych niższego poziomu.

Podobnie można używać funkcji lambda treści @Composable, aby uzyskać te same korzyści:

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

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