CompositionLocal to narzędzie do przekazywania danych w kompozycji w sposób niejawny. Na tej stronie dowiesz się więcej o CompositionLocal, jak utworzyć własny CompositionLocal oraz czy CompositionLocal jest dobrym rozwiązaniem w Twoim przypadku.
Wprowadzenie do CompositionLocal
Zazwyczaj w Compose, dane przepływają w dół przez drzewo interfejsu jako parametry każdej funkcji typu „composable”. 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 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. Wartość ta może być używana przez funkcje typu „composable” potomne 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 kompozycyjnych.
- Drzewo interfejsu lub hierarchia interfejsu to drzewo
LayoutNodeutworzone, zaktualizowane i utrzymywane przez proces kompozycji.
CompositionLocal jest tym, czego używa motyw Material.
MaterialTheme to obiekt, który udostępnia 3 instancje CompositionLocal: colorScheme, typography i shapes, dzięki czemu możesz je później pobrać w dowolnej części kompozycji.
Są to właściwości LocalColorScheme, LocalShapes i LocalTypography, do których możesz uzyskać dostęp za pomocą atrybutów colorScheme, shapes i typography w 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
elementu CompositionLocal odpowiada najbliższej wartości podanej przez
element nadrzędny w tej części kompozycji.
Aby podać nową wartość w CompositionLocal, użyj
CompositionLocalProvider
i jego provides
funkcji wrostkowej, która łączy klucz CompositionLocal z value. Lambda
content w CompositionLocalProvider otrzyma podaną
wartość, gdy uzyska dostęp do właściwości current w 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 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") }
CompositionLocalExample.W ostatnim przykładzie instancje CompositionLocal były wewnętrznie używane 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 w
LocalContext CompositionLocal, która jest powszechnie używana 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ści
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 podano wartość current. Narzędzia takie jak Find
usages w IDE lub inspektor układu Compose zapewniają wystarczającą
ilość informacji, aby złagodzić 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 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 wartość CompositionLocal nie jest podana.
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 CompositionLocal, ponieważ zawsze będzie wymagać jawnego podania wartości.
Unikaj CompositionLocal w przypadku koncepcji, które nie są traktowane jako o zasięgu 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ń, zapoznaj się z
sekcją Alternatywne rozwiązania przed utworzeniem
CompositionLocal.
Przykładem złej praktyki jest utworzenie CompositionLocal, która 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 docompositionLocalOfodczytystaticCompositionLocalOfnie są śledzone przez Compose. Zmiana wartości powoduje ponowne skomponowanie całej lambdycontent, w której podanoCompositionLocal, a nie tylko miejsc, w których odczytywana jest wartośćcurrentw kompozycji.
Jeśli wartość podana w CompositionLocal raczej się nie zmieni 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 komponencie interfejsu. Ponieważ różne podniesienia w aplikacji powinny się rozprzestrzeniać 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ść w 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ść w tym 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 } }
Alternatywne rozwiązania
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.
Oto 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ż oddziela 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() } }
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Skład motywu w Compose
- Używanie widoków w Compose
- Kotlin w Jetpack Compose