Dane o zakresie lokalnym dzięki CompositionLocal

CompositionLocal to narzędzie do przekazywania danych w kompozycji w sposób niejawny. Na tej stronie dowiesz się więcej o tym, czym jest CompositionLocal, jak utworzyć własny CompositionLocal oraz czy CompositionLocal jest dobrym rozwiązaniem w Twoim przypadku.

Wprowadzenie do CompositionLocal

Zwykle w Compose, dane przepływają w dół przez drzewo interfejsu jako parametry każdej funkcji kompozycyjnej. Dzięki temu zależności funkcji kompozycyjnej są jawne. Może to jednak być uciążliwe w przypadku danych, które są używane bardzo często i w wielu miejscach, takich jak kolory czy style typografii. Zobacz ten przykład:

@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, która umożliwia tworzenie nazwanych obiektów o zasięgu drzewa, które mogą być używane jako niejawny sposób przepływu danych przez drzewo interfejsu.

Elementy CompositionLocal zwykle mają wartość w określonym węźle drzewa interfejsu. Ta wartość może być używana przez jej funkcje typu „composable” potomne bez deklarowania CompositionLocal jako parametru w funkcji typu „composable”.

CompositionLocal jest tym, czego motyw Material używa pod maską. MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typography i shapes, co pozwala na ich późniejsze pobranie w dowolnej części kompozycji. Są to właściwości LocalColorScheme, LocalShapes i LocalTypography, do których można uzyskać dostęp za pomocą atrybutów colorScheme, shapes i typography obiektu MaterialTheme.

@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 kompozycji , dzięki czemu możesz podawać różne wartości na różnych poziomach drzewa. Wartość current obiektu 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 jego funkcji wrostkowej provides , która łączy klucz CompositionLocal z value. Lambda content obiektu CompositionLocalProvider otrzyma podaną wartość, gdy uzyska dostęp do właściwości current obiektu 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 ikonografii, aby zapewnić kontrast z bieżącym kolorem tła. W tym przykładzie CompositionLocalProvider służy do podawania różnych wartości w różnych częściach 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 funkcje kompozycyjne Material. Aby uzyskać dostęp do bieżącej wartości CompositionLocal, użyj jej current właściwości. W tym przykładzie do formatowania tekstu używana jest bieżąca wartość Context obiektu 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)
}

Tworzenie własnego CompositionLocal

CompositionLocal to narzędzie do przekazywania danych w kompozycji w sposób niejawny.

Kolejnym kluczowym sygnałem do użycia CompositionLocal jest sytuacja, gdy parametr jest przekrojowy, a warstwy pośrednie 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 pod maską. Funkcja kompozycyjna selektora multimediów może dodawać nowe funkcje umożliwiające dostęp do treści chronionych uprawnieniami na urządzeniu bez zmiany interfejsu API i bez konieczności informowania wywołujących selektor multimediów o tym dodatkowym kontekście używanym ze środowiska.

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

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

Ponadto może nie być jasnego źródła prawdy dla tej zależności, ponieważ może się ona zmieniać 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 sprawdzić, gdzie została podana wartość current. Narzędzia takie jak Znajdź użycia w IDE lub inspektor układu Compose zapewniają wystarczającą ilość informacji, aby rozwiązać ten problem.

Decydowanie, czy używać 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 zagwarantować, że deweloperowi będzie niezwykle trudno znaleźć się w sytuacji, w której wartość CompositionLocal nie jest podana. Niepodanie wartości domyślnej może powodować problemy i frustrację podczas tworzenia testów lub wyświetlania podglądu funkcji kompozycyjnej, która używa tego CompositionLocal, ponieważ zawsze będzie wymagać jawnego podania wartości.

Unikaj CompositionLocal w przypadku koncepcji, które nie są uważane za ograniczone do drzewa lub podhierarchii. CompositionLocal ma sens, gdy może być potencjalnie używany przez dowolny element potomny, 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 złej praktyki jest utworzenie CompositionLocal, który zawiera ViewModel konkretnego ekranu, aby wszystkie funkcje kompozycyjne na tym ekranie mogły uzyskać odniesienie do ViewModel w celu wykonania pewnej logiki. Jest to zła praktyka, ponieważ nie wszystkie funkcje kompozycyjne poniżej określonego drzewa interfejsu muszą znać ViewModel. Dobrą praktyką jest przekazywanie do funkcji kompozycyjnych tylko tych informacji , których potrzebują, zgodnie z wzorcem, w którym stan przepływa w dół, a zdarzenia w górę. Dzięki temu funkcje kompozycyjne będą bardziej wielokrotnego użytku i łatwiejsze do testowania.

Tworzenie CompositionLocal

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

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

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

Jeśli wartość podana w CompositionLocal jest mało prawdopodobna lub nigdy się nie zmieni, użyj staticCompositionLocalOf, aby zwiększyć wydajność.

Na przykład system projektowania aplikacji może mieć określony sposób podnoszenia funkcji kompozycyjnych za pomocą cienia w przypadku komponentu interfejsu. Ponieważ różne podniesienia w aplikacji powinny propagować się w całym drzewie interfejsu, używamy CompositionLocal. Ponieważ wartość CompositionLocal jest wyprowadzana warunkowo na podstawie motywu systemu, 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() }

Podawanie wartości w CompositionLocal

Funkcja kompozycyjna CompositionLocalProvider wiąże wartości z instancjami CompositionLocal w danej hierarchii. Aby podać nową wartość dla CompositionLocal, użyj funkcji wrostkowej provides , która łączy 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
            }
        }
    }
}

Używanie CompositionLocal

CompositionLocal.current zwraca wartość podaną przez najbliższy CompositionLocalProvider, który podaje wartość dla tego 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

CompositionLocal może być nadmiernym rozwiązaniem w niektórych przypadkach. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy używać CompositionLocal, inne rozwiązanie może być lepiej dopasowane do Twojego przypadku.

Przekazywanie jawnych parametrów

Jawne określanie zależności funkcji kompozycyjnej to dobry nawyk. Zalecamy, aby funkcje kompozycyjne otrzymywały tylko to, czego potrzebują. Aby zachęcić do rozdzielania i ponownego używania funkcji kompozycyjnych, każda funkcja kompozycyjna 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 potomnego, który przyjmuje zależność w celu wykonania pewnej logiki, robi to element nadrzędny.

Zobacz ten przykład, w którym element potomny 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ć wiele obowiązków. Ponadto przekazywanie MyViewModel jako zależności sprawia, że MyDescendant jest mniej wielokrotnego użytku, ponieważ są teraz ze sobą powiązane. Rozważ alternatywę, która nie przekazuje zależności do elementu potomnego i używa zasad odwrócenia sterowania, dzięki czemu element nadrzędny jest odpowiedzialny za wykonanie 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 dopasowane do niektórych przypadków użycia, ponieważ odłącza element podrzędny od jego bezpośrednich elementów nadrzędnych. Funkcje kompozycyjne nadrzędne stają się bardziej złożone, aby mieć bardziej elastyczne funkcje kompozycyjne niższego poziomu.

Podobnie lambdy treści @Composable mogą być używane w ten sam sposób, 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()
    }
}