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