Dane o zakresie lokalnym dzięki CompositionLocal

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

Przedstawiamy CompositionLocal

Zwykle w Compose dane przepływają w dół drzewa 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ą 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 komponentów, Compose oferuje CompositionLocal, co umożliwia tworzenie nazwanych obiektów o zakresie 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. Wartość ta może być używana przez elementy kompozycyjne będące jej elementami podrzędnymi bez deklarowania CompositionLocal jako parametru w funkcji kompozycyjnej.

CompositionLocal to element, którego używa motyw Material. MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typographyshapes. Możesz je później pobrać w dowolnej części podrzędnej kompozycji. Są to właściwości LocalColorScheme, LocalShapesLocalTypography, do których możesz uzyskać dostęp za pomocą atrybutów MaterialTheme colorScheme, shapestypography.

@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, więc 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 funkcji wrostkowej CompositionLocalProvider i jej funkcji provides, która przypisuje klucz CompositionLocal do value. Funkcja lambda content w obiekcie CompositionLocalProvider otrzyma podaną wartość podczas uzyskiwania dostępu do właściwości current obiektu CompositionLocal. Gdy podana zostanie 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 przypadku tekstu i ikon, aby zapewnić kontrast z bieżącym kolorem tła. W poniższym przykładzie symbol 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")
}

Rysunek 1. Podgląd funkcji 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 elementu CompositionLocal, użyj jego 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 często 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 dół przez kompozycję w sposób niejawny.

Kolejnym kluczowym sygnałem do użycia CompositionLocal jest sytuacja, w której parametr jest przekrojowy, a warstwy pośrednie implementacji nie powinny wiedzieć o jego istnieniu, ponieważ uświadomienie tych warstw pośrednich ograniczyłoby użyteczność funkcji kompozycyjnej. Na przykład zapytanie o uprawnienia Androida jest realizowane za pomocą CompositionLocal. Kompozycja 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 wymagania od wywołujących selektor multimediów, aby znali ten dodatkowy kontekst używany w środowisku.

Nie zawsze jest to jednak najlepsze rozwiązanie.CompositionLocal Odradzamy nadmierne używanie CompositionLocal, ponieważ ma to pewne wady:

CompositionLocal utrudnia zrozumienie działania funkcji kompozycyjnej. Tworzą one zależności niejawne, więc wywołujący funkcje kompozycyjne, które ich używają, muszą się upewnić, że każda wartość CompositionLocal jest spełniona.

Ponadto może nie być jasnego źródła informacji o tej zależności, ponieważ może się ona zmieniać w dowolnej części kompozycji. Dlatego debugowanie aplikacji, gdy wystąpi problem, może być trudniejsze, ponieważ musisz przejść w górę kompozycji, aby sprawdzić, gdzie podano wartość current. Narzędzia takie jak Find usages (Znajdź użycia) w IDE lub inspektor układu Compose dostarczają wystarczających 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 powinna 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 nie podano wartości parametru CompositionLocal. Brak 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 elementu CompositionLocal i zawsze wymaga jego wyraźnego podania.

Unikaj używania znaku CompositionLocal w przypadku pojęć, które nie są uważane za ograniczone do drzewa lub podhierarchii. CompositionLocal ma sens, gdy może być potencjalnie używany przez wszystkich potomków, a nie tylko przez kilku z nich.

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

Przykładem nieodpowiedniej metody jest utworzenie CompositionLocal, które zawiera ViewModel konkretnego ekranu, aby wszystkie funkcje kompozycyjne na tym ekranie mogły uzyskać odwołanie do ViewModel i wykonać pewną logikę. To zła praktyka, ponieważ nie wszystkie funkcje kompozycyjne w określonym drzewie interfejsu muszą znać wartość ViewModel. Sprawdzoną metodą jest przekazywanie do funkcji kompozycyjnych tylko tych informacji, które są im potrzebne, zgodnie z wzorcem stan przepływa w dół, a zdarzenia w górę. Dzięki temu Twoje 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 ponownego komponowania 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 funkcji lambda content, w której podano wartość CompositionLocal, a nie tylko miejsc, w których w kompozycji odczytywana jest wartość current.

Jeśli wartość przekazywana do CompositionLocal raczej się nie zmieni lub nigdy się nie zmieni, użyj staticCompositionLocalOf, aby zwiększyć skuteczność.

Na przykład system projektowania aplikacji może narzucać sposób, w jaki elementy kompozycyjne są wyróżniane za pomocą cienia w komponencie interfejsu. Różne poziomy uniesienia aplikacji powinny być propagowane w całym drzewie interfejsu, dlatego używamy CompositionLocal. Wartość CompositionLocal jest uzyskiwana warunkowo na podstawie motywu systemu, dlatego używamy interfejsu 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() }

Podawanie wartości do 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 przypisuje klucz CompositionLocal do 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
            }
        }
    }
}

Korzystanie z CompositionLocal

Funkcja CompositionLocal.current zwraca wartość podaną przez najbliższą funkcję CompositionLocalProvider, która dostarcza wartość do tej funkcji 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 Deciding whether to use CompositionLocal (Decydujemy, czy użyć CompositionLocal), inne rozwiązanie może być lepiej dopasowane do Twojego przypadku użycia.

Przekazywanie parametrów jawnych

Jawne określanie zależności funkcji kompozycyjnych to dobry nawyk. Zalecamy przekazywanie do funkcji kompozycyjnych tylko tych danych, których potrzebują. Aby zachęcić do rozdzielania i ponownego wykorzystywania 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 kontroli

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

Poniższy przykład pokazuje, jak 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 ponosić dużą odpowiedzialność. Ponadto przekazywanie MyViewModel jako zależności sprawia, że MyDescendant jest mniej wielokrotnego użytku, ponieważ są one teraz ze sobą powiązane. Rozważ alternatywę, która nie przekazuje zależności do elementu podrzędnego i wykorzystuje zasady odwrócenia kontroli, dzięki czemu 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")
    }
}

W niektórych przypadkach może to być lepsze rozwiązanie, ponieważ odłącza element podrzędny od jego bezpośrednich elementów nadrzędnych. Komponenty kompozycyjne przodków stają się bardziej złożone, aby zapewnić większą elastyczność komponentów kompozycyjnych niższego poziomu.

Podobnie @Composable lambdy treści 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()
    }
}