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
, typography
i shapes
. Możesz 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żesz 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 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 docompositionLocalOf
odczytystaticCompositionLocalOf
nie są śledzone przez Compose. Zmiana wartości powoduje ponowne skomponowanie całej funkcji lambdacontent
, 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() } }
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Składnia motywu w Compose
- Korzystanie z widoków w Compose
- Kotlin w Jetpack Compose