CompositionLocal
— это инструмент для неявной передачи данных через композицию. На этой странице вы подробнее узнаете о 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
, к которым можно получить доступ через атрибуты colorScheme
, shapes
и typography
темы 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 ) }
Экземпляр CompositionLocal
ограничен частью композиции, что позволяет предоставлять разные значения на разных уровнях дерева. current
значение CompositionLocal
соответствует ближайшему значению, предоставленному предком в этой части композиции.
Чтобы предоставить новое значение CompositionLocal
, используйте CompositionLocalProvider
и его инфиксную функцию provides
, которая связывает ключ CompositionLocal
со value
. Лямбда-функция content
CompositionLocalProvider
получит предоставленное значение при доступе к current
свойству CompositionLocal
. При предоставлении нового значения Compose перекомпонует части Composition, считывающие CompositionLocal
.
Например, свойство LocalContentColor
CompositionLocal
содержит предпочтительный цвет контента, используемый для текста и иконок, чтобы обеспечить его контрастность с текущим цветом фона. В следующем примере CompositionLocalProvider
используется для предоставления различных значений для разных частей композиции.
@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
значение. Такие инструменты, как «Поиск использований» в IDE или «Инспектор макета Compose», предоставляют достаточно информации для решения этой проблемы.
Решение об использовании CompositionLocal
Существуют определенные условия, которые могут сделать CompositionLocal
хорошим решением для вашего варианта использования:
У CompositionLocal
должно быть подходящее значение по умолчанию . Если значение по умолчанию отсутствует, необходимо гарантировать, что разработчику будет крайне сложно попасть в ситуацию, когда значение для CompositionLocal
не указано. Отсутствие значения по умолчанию может привести к проблемам и разочарованию при создании тестов или предварительном просмотре компонуемого объекта, использующего этот CompositionLocal
, поскольку он всегда будет требовать явного указания.
Избегайте использования CompositionLocal
для концепций, которые не рассматриваются как имеющие область действия в пределах дерева или подиерархии . CompositionLocal
имеет смысл, когда его потенциально может использовать любой потомок, а не только некоторые из них.
Если ваш вариант использования не соответствует этим требованиям, перед созданием CompositionLocal
ознакомьтесь с разделом «Альтернативные варианты для рассмотрения» .
Примером плохой практики является создание объекта CompositionLocal
, содержащего ViewModel
конкретного экрана, чтобы все компонуемые элементы на этом экране могли получить ссылку на ViewModel
для выполнения некоторой логики. Это плохая практика, поскольку не всем компонуемым элементам ниже конкретного дерева пользовательского интерфейса требуется информация о ViewModel
. Хорошей практикой является передача компонуемым элементам только необходимой им информации, следуя шаблону « состояние передается вниз, а события — вверх» . Такой подход сделает ваши компонуемые элементы более удобными для повторного использования и более простыми в тестировании.
Создание CompositionLocal
Существует два API для создания CompositionLocal
:
compositionLocalOf
: Изменение значения, предоставленного во время перекомпоновки, делает недействительным только то содержимое, которое считывает егоcurrent
значение.staticCompositionLocalOf
: В отличие отcompositionLocalOf
, чтениеstaticCompositionLocalOf
не отслеживается Compose. Изменение значения приводит к перекомпоновке всегоcontent
лямбда-выражения, где предоставляетсяCompositionLocal
, а не только тех мест в Composition, где считываетсяcurrent
значение.
Если значение, предоставленное 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
привязывает значения к экземплярам CompositionLocal
для заданной иерархии . Чтобы предоставить новое значение объекту CompositionLocal
, используйте инфиксную функцию provides
, которая связывает ключ 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
контента можно использовать таким же образом и получить те же преимущества :
@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