CompositionLocal
— это инструмент для неявной передачи данных вниз через Composition. На этой странице вы узнаете, что такое CompositionLocal
более подробно, как создать свой собственный CompositionLocal
и узнаете, является ли CompositionLocal
хорошим решением для вашего варианта использования.
Представляем CompositionLocal
Обычно в Compose данные проходят через дерево пользовательского интерфейса в качестве параметров для каждой компонуемой функции. Это делает зависимости компонуемой функции явными. Однако это может быть обременительно для данных, которые используются очень часто и широко, например, цвета или стили шрифтов. Смотрите следующий пример:
@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 ) }
Чтобы избежать необходимости передавать цвета в качестве явной зависимости параметров для большинства компонуемых объектов, Compose предлагает CompositionLocal
, позволяющий создавать именованные объекты с областью действия дерева, которые можно использовать в качестве неявного способа передачи данных через дерево пользовательского интерфейса.
Элементы CompositionLocal
обычно предоставляются со значением в определенном узле дерева пользовательского интерфейса. Это значение может использоваться его составными потомками без объявления CompositionLocal
как параметра в составной функции.
CompositionLocal
— это то, что тема Material использует под капотом. MaterialTheme
— это объект, который предоставляет три экземпляра CompositionLocal
: colorScheme
, typography
и shapes
, позволяя вам извлекать их позже в любой дочерней части Composition. В частности, это свойства LocalColorScheme
, LocalShapes
и LocalTypography
, к которым вы можете получить доступ через атрибуты MaterialTheme
colorScheme
, shapes
и 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 ) }
Экземпляр CompositionLocal
ограничен частью Composition, поэтому вы можете предоставлять различные значения на разных уровнях дерева. current
значение CompositionLocal
соответствует ближайшему значению, предоставленному предком в этой части Composition.
Чтобы предоставить новое значение CompositionLocal
, используйте CompositionLocalProvider
и его функцию provides
infix , которая связывает ключ CompositionLocal
со value
. Лямбда-функция content
CompositionLocalProvider
получит предоставленное значение при доступе к current
свойству CompositionLocal
. Когда предоставляется новое значение, Compose перекомпоновывает части Composition, которые считывают CompositionLocal
.
В качестве примера этого, LocalContentColor
CompositionLocal
содержит предпочтительный цвет контента, используемый для текста и иконографии, чтобы гарантировать его контраст с текущим цветом фона. В следующем примере CompositionLocalProvider
используется для предоставления различных значений для различных частей Composition.
@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") }
Рисунок 1. Предварительный просмотр компонуемого объекта CompositionLocalExample
.
В последнем примере экземпляры CompositionLocal
использовались внутренне компонуемыми элементами Material. Чтобы получить доступ к текущему значению CompositionLocal
, используйте его current
свойство. В следующем примере текущее значение Context
для LocalContext
CompositionLocal
, которое обычно используется в приложениях Android, используется для форматирования текста:
@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) }
Создание собственной CompositionLocal
CompositionLocal
— это инструмент для неявной передачи данных через композицию .
Другим ключевым сигналом для использования CompositionLocal
является ситуация, когда параметр является сквозным и промежуточные уровни реализации не должны знать о его существовании , поскольку информирование этих промежуточных уровней ограничит полезность компонуемого объекта. Например, запрос разрешений Android осуществляется CompositionLocal
под капотом. Компонуемый медиа-пикер может добавлять новые функциональные возможности для доступа к защищенному разрешениями контенту на устройстве без изменения его API и без необходимости для вызывающих медиа-пикеров знать об этом добавленном контексте, используемом из среды.
Однако CompositionLocal
не всегда является лучшим решением. Мы не рекомендуем чрезмерно использовать CompositionLocal
, поскольку он имеет некоторые недостатки:
CompositionLocal
затрудняет понимание поведения компонуемых объектов . Поскольку они создают неявные зависимости, вызывающие компонуемые объекты, которые их используют, должны убедиться, что значение для каждого CompositionLocal
удовлетворено.
Более того, может не быть четкого источника истины для этой зависимости, поскольку она может мутировать в любой части Composition. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно будет перемещаться вверх по Composition, чтобы увидеть, где было предоставлено current
значение. Такие инструменты, как Find usages в IDE или Compose layout inspector, предоставляют достаточно информации для смягчения этой проблемы.
Решение об использовании CompositionLocal
Существуют определенные условия, которые могут сделать CompositionLocal
хорошим решением для вашего варианта использования:
CompositionLocal
должен иметь хорошее значение по умолчанию . Если значения по умолчанию нет, вы должны гарантировать, что разработчику будет крайне сложно попасть в ситуацию, когда значение для CompositionLocal
не указано. Не предоставление значения по умолчанию может вызвать проблемы и разочарование при создании тестов или предварительном просмотре компонуемого объекта, который использует этот CompositionLocal
, всегда будет требовать его явного указания.
Избегайте CompositionLocal
для концепций, которые не рассматриваются как имеющие область действия в виде дерева или подиерархии . CompositionLocal
имеет смысл, когда его может потенциально использовать любой потомок, а не несколько из них.
Если ваш вариант использования не соответствует этим требованиям, ознакомьтесь с разделом «Альтернативы для рассмотрения» перед созданием CompositionLocal
.
Примером плохой практики является создание CompositionLocal
, который содержит ViewModel
определенного экрана, чтобы все компонуемые элементы на этом экране могли получить ссылку на ViewModel
для выполнения некоторой логики. Это плохая практика, поскольку не все компонуемые элементы ниже определенного дерева пользовательского интерфейса должны знать о ViewModel
. Хорошей практикой является передача компонуемым элементам только той информации, которая им нужна, следуя шаблону, что состояние течет вниз, а события текут вверх . Такой подход сделает ваши компонуемые элементы более пригодными для повторного использования и более простыми для тестирования.
Создание CompositionLocal
Для создания CompositionLocal
существует два API:
compositionLocalOf
: Изменение значения, предоставленного во время повторной композиции, делает недействительным только то содержимое, которое считывает егоcurrent
значение.staticCompositionLocalOf
: В отличие отcompositionLocalOf
, чтенияstaticCompositionLocalOf
не отслеживаются Compose. Изменение значения приводит к перекомпоновке всегоcontent
лямбда, где предоставляетсяCompositionLocal
, а не только мест, гдеcurrent
значение считывается в Composition.
Если значение, предоставленное CompositionLocal
вряд ли изменится или никогда не изменится, используйте staticCompositionLocalOf
, чтобы получить выигрыш в производительности.
Например, система дизайна приложения может быть упрямой в том, как компонуемые элементы поднимаются с помощью тени для компонента пользовательского интерфейса. Поскольку различные высоты для приложения должны распространяться по всему дереву пользовательского интерфейса, мы используем CompositionLocal
. Поскольку значение CompositionLocal
выводится условно на основе темы системы, мы используем 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() }
Предоставление значений CompositionLocal
CompositionLocalProvider
composable связывает значения с экземплярами CompositionLocal
для заданной иерархии . Чтобы предоставить новое значение CompositionLocal
, используйте функцию provides
infix, которая связывает ключ CompositionLocal
со value
следующим образом:
// 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 } } } }
Потребление CompositionLocal
CompositionLocal.current
возвращает значение, предоставленное ближайшим CompositionLocalProvider
, который предоставляет значение для этого 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 } }
Альтернативы для рассмотрения
CompositionLocal
может быть избыточным решением для некоторых вариантов использования. Если ваш вариант использования не соответствует критериям, указанным в разделе «Решение об использовании CompositionLocal» , другое решение, вероятно, лучше подойдет для вашего варианта использования.
Передача явных параметров
Явное указание зависимостей компонуемых объектов — хорошая привычка. Мы рекомендуем передавать компонуемым объектам только то, что им нужно . Чтобы поощрять разделение и повторное использование компонуемых объектов, каждый компонуемый объект должен содержать минимально возможное количество информации.
@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 }
Инверсия управления
Другой способ избежать передачи ненужных зависимостей в компонуемый объект — инверсия управления . Вместо того, чтобы потомок принимал зависимость для выполнения некоторой логики, это делает родитель.
Посмотрите на следующий пример, где потомку необходимо инициировать запрос на загрузку некоторых данных:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
В зависимости от случая, MyDescendant
может иметь большую ответственность. Кроме того, передача MyViewModel
как зависимости делает MyDescendant
менее пригодным для повторного использования, поскольку теперь они связаны друг с другом. Рассмотрим альтернативу, которая не передает зависимость потомку и использует принципы инверсии управления, которые возлагают на предка ответственность за выполнение логики:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Этот подход может быть более подходящим для некоторых случаев использования, поскольку он отделяет потомка от его непосредственных предков . Предковые компонуемые объекты, как правило, становятся более сложными в пользу наличия более гибких компонуемых объектов более низкого уровня.
Аналогично, лямбда-выражения @Composable
content могут использоваться таким же образом для получения тех же преимуществ :
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Анатомия темы в Compose
- Использование представлений в Compose
- Kotlin для Jetpack Compose