CompositionLocal
to narzędzie do przekazywania danych w składowej w sposób domyślny. Z tej strony dowiesz się więcej o tym, czym jest CompositionLocal
, jak utworzyć własną CompositionLocal
i czy CompositionLocal
to dobre rozwiązanie w Twoim przypadku.
Przedstawiamy CompositionLocal
Zwykle w Compose dane przepływają w dół przez drzewo interfejsu użytkownika jako parametry każdej funkcji kompozytowej. Dzięki temu zależności kompozytowe są jawne. Może to jednak być uciążliwe w przypadku danych, które są często i powszechnie używane, np. kolorów czy stylów czcionek. 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 komponentów, Compose udostępnia parametr CompositionLocal
, który umożliwia tworzenie obiektów nazwanych w zakresie drzewa, które można używać jako domyślny sposób przepływu danych przez drzewo interfejsu użytkownika.
Elementy CompositionLocal
są zwykle dostarczane z wartością w określonym węźle drzewa interfejsu. Ta wartość może być używana przez potomków funkcji składanej bez deklarowania CompositionLocal
jako parametru w funkcji składanej.
CompositionLocal
to to, czego używa motyw Material.
MaterialTheme
to obiekt, który udostępnia 3 instancje CompositionLocal
: colorScheme
, typography
i shapes
, co pozwala na ich późniejsze pobieranie w dowolnej części potomnej kompozycji.
Dotyczy to w szczególności 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 ) }
Występ CompositionLocal
jest ograniczony do części kompozycji, dzięki czemu możesz podawać różne wartości na różnych poziomach drzewa. Wartość current
w 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 CompositionLocalProvider
i jej funkcji w nawiasach provides
, która łączy klucz CompositionLocal
z value
. Lambda content
obiektu CompositionLocalProvider
otrzyma podawaną wartość podczas uzyskiwania dostępu do właściwości current
obiektu CompositionLocal
. Gdy podasz nową wartość, Compose ponownie utworzy części kompozycji, które odczytują CompositionLocal
.
Przykładowo atrybuty LocalContentColor
CompositionLocal
zawierają preferowany kolor treści używany do tekstu i ikonografii, aby zapewnić kontrast z bieżącym kolorem tła. W tym przykładzie zmienna 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 komponentu 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 właściwości current
. W tym przykładzie do formatowania 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łasnych CompositionLocal
CompositionLocal
to narzędzia do przekazywania danych w ramach kompozycji w sposób domyślny.
Kolejnym sygnałem wskazującym na użycie CompositionLocal
jest sytuacja, w której parametr jest elementem pośrednim, a warstwy pośrednie implementacji nie powinny o nim wiedzieć, ponieważ ujawnienie tej informacji ograniczyłoby użyteczność komponentu. Na przykład zapytanie o uprawnienia Androida jest obsługiwane przez CompositionLocal
. Komponent selektora multimediów może dodawać nowe funkcje, aby uzyskać dostęp do treści chronionych hasłem na urządzeniu bez zmiany interfejsu API i wymagania od wywołujących selektor multimediów znajomości tego dodanego kontekstu używanego w środowisku.
Jednak CompositionLocal
nie zawsze jest najlepszym rozwiązaniem. Nie zalecamy nadużywania funkcji CompositionLocal
, ponieważ ma ona pewne wady:
CompositionLocal
utrudnia analizowanie zachowania komponentu. Ponieważ wywołania komponentów, które ich używają, tworzą ukryte zależności, wywołujący muszą się upewnić, że wartość każdego CompositionLocal
jest spełniona.
Ponadto może nie być wyraźnego źródła informacji o 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ę w składowej, aby sprawdzić, gdzie podano wartość current
. Narzędzia takie jak Znajdź użycie w IDE lub Inspektor układu w Compose zawierają wystarczającą ilość informacji, aby zbadać ten problem.
Decydowanie o użyciu CompositionLocal
W pewnych przypadkach CompositionLocal
może być dobrym rozwiązaniem:
Parametr CompositionLocal
powinien mieć odpowiednią wartość domyślną. Jeśli nie ma wartości domyślnej, musisz zagwarantować, że bardzo trudno będzie deweloperowi 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 podglądu komponentu, który używa tej funkcji. CompositionLocal
będzie zawsze wymagać podania tej wartości.
Unikaj CompositionLocal
w przypadku pojęć, które nie są ograniczone do drzewa lub podhierarchii. CompositionLocal
ma sens, gdy może być potencjalnie używany przez dowolnego potomka, a nie tylko przez kilku z nich.
Jeśli Twoje zastosowanie nie spełnia tych wymagań, przed utworzeniem CompositionLocal
zapoznaj się z sekcją Możliwe rozwiązania.
Przykładem nieodpowiedniej metody jest tworzenie CompositionLocal
, które zawiera ViewModel
danego ekranu, aby wszystkie komponenty na tym ekranie mogły uzyskać odwołanie do ViewModel
w celu wykonania pewnych operacji. Jest to zła praktyka, ponieważ nie wszystkie komponenty w drzewie interfejsu użytkownika muszą znać wartość ViewModel
. Dobrą praktyką jest przekazywanie komponentom tylko tych informacji, których potrzebują, zgodnie ze schematem, w którym stan przepływa w dół, a zdarzenia w górę. Dzięki temu komponenty będą bardziej przydatne i łatwiejsze do testowania.
Tworzenie CompositionLocal
Do tworzenia CompositionLocal
można użyć 2 interfejsów API:
compositionLocalOf
: zmiana wartości podawanej podczas rekompozycji unieważnia tylko treści, które odczytują wartośćcurrent
.staticCompositionLocalOf
: w odróżnieniu odcompositionLocalOf
, usługa Compose nie śledzi odczytów funkcjistaticCompositionLocalOf
. Zmiana wartości powoduje ponowne złożenie całej funkcji lambdacontent
, w której podana jest wartośćCompositionLocal
, a nie tylko miejsc, w których wartośćcurrent
jest odczytywana w kompozycji.
Jeśli wartość podana w polu CompositionLocal
jest mało prawdopodobna do zmiany lub nigdy się nie zmieni, użyj pola staticCompositionLocalOf
, aby uzyskać korzyści związane ze skutecznością.
Na przykład system projektowania aplikacji może być subiektywny w sposobie, w jaki komponenty składane są wyróżniane za pomocą cienia dla komponentu interfejsu użytkownika. Różne poziomy aplikacji powinny być propagowane w drzewie interfejsu użytkownika, dlatego używamy CompositionLocal
. Ponieważ wartość CompositionLocal
jest wyprowadzana warunkowo na podstawie motywu systemowego, 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 obszarze CompositionLocal
Komponent CompositionLocalProvider
przypisuje wartości do instancji CompositionLocal
w danej hierarchii. Aby podać nową wartość CompositionLocal
, użyj funkcji provides
infix, która wiąże klucz CompositionLocal
z elementem 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 } } } }
Wykorzystywanie CompositionLocal
Funkcja CompositionLocal.current
zwraca wartość podaną przez najbliższe wyrażenie CompositionLocalProvider
, które dostarcza wartość dla argumentu 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 } }
Możliwe rozwiązania
CompositionLocal
może być nieodpowiednim rozwiązaniem w niektórych przypadkach użycia. Jeśli Twój przypadek użycia nie spełnia kryteriów określonych w sekcji Decydowanie, czy użyć CompositionLocal, inne rozwiązanie może być lepsze do Twojego przypadku użycia.
Przekazywanie parametrów jawnych
Dobrą praktyką jest wyraźne określenie zależności komponentów. Zalecamy, aby przekazywać komponentom tylko to, czego potrzebują. Aby zachęcić do rozdzielania i ponownego używania komponentów, każdy z nich powinien 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 unikanie przekazywania niepotrzebnych zależności do komponentu jest inwersja kontroli. Zamiast tego, aby potomek przyjmował zależność do wykonania określonej logiki, wykonuje to element nadrzędny.
W tym przykładzie potomek musi wywołać żądanie, aby załadować dane:
@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 sytuacji MyDescendant
może mieć wiele obowiązków. Ponadto przekazywanie MyViewModel
jako zależności powoduje, że MyDescendant
jest mniej użyteczny, ponieważ te elementy są teraz ze sobą połączone. Rozważ rozwiązanie, które nie przekazuje zależności potomkowi i korzysta z inwersji zasad kontroli, dzięki której rodzic 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") } }
Takie podejście może być odpowiednie w niektórych przypadkach, ponieważ odłącza dziecko od jego bezpośrednich przodków. Składniki nadrzędne stają się coraz bardziej złożone, ponieważ dają możliwość tworzenia bardziej elastycznych składników na niższych poziomach.
Podobnie @Composable
lambda treści może być używana w taki 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() } }
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Składnia motywu w sekcji Tworzenie
- Korzystanie z widoków w sekcji Tworzenie wiadomości
- Kotlin w Jetpack Compose