Данные с локальной областью видимости с помощью CompositionLocal

CompositionLocal – это инструмент для неявной передачи данных через композицию. На этой странице вы узнаете, что такое CompositionLocal, как создать собственный CompositionLocal и подходит ли CompositionLocal для вашего случая.

Общие сведения о CompositionLocal

Обычно в Compose данные передаются вниз по дереву пользовательского интерфейса в качестве параметров для каждой composable-функции. Это делает зависимости 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
    )
}

Чтобы не передавать цвета в качестве явной зависимости параметров большинству composable-функций, в Compose есть CompositionLocal. С помощью этого инструмента можно создавать именованные объекты в области дерева, которые позволяют неявно передавать данные по дереву пользовательского интерфейса.

Элементы CompositionLocal обычно имеют значение в определенном узле дерева интерфейса. Это значение может использоваться дочерними composable-функциями без объявления CompositionLocal в качестве параметра.

Основные термины. В этом руководстве используются термины Композиция, Дерево пользовательского интерфейса и Иерархия пользовательского интерфейса. Хотя в других руководствах они могут использоваться как взаимозаменяемые, у них разные значения:

  • Композиция – это запись графа вызовов composable-функций.
  • Дерево или иерархия пользовательского интерфейса – это дерево LayoutNode, созданное, обновленное и поддерживаемое процессом компоновки.

CompositionLocal – это то, что используется в тематическом оформлении Material Design. 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 ограничен частью композиции, поэтому вы можете указывать разные значения на разных уровнях дерева. Значение 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")
}

Предварительный просмотр composable-функции CompositionLocalExample.
Рисунок 1. Предварительный просмотр composable-функции CompositionLocalExample.

В последнем примере экземпляры CompositionLocal использовались внутри composable-функций 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 – это инструмент для неявной передачи данных через Composition.

Ещё один важный сигнал для использования CompositionLocal – когда параметр является сквозным и промежуточные уровни реализации не должны знать о его существовании, поскольку это ограничило бы полезность composable-функции. Например, запрос разрешений Android выполняется с помощью CompositionLocal. Composable-функция инструмента выбора медиа может добавлять новые функции для доступа к защищенному разрешениями контенту на устройстве без изменения API и без необходимости для вызывающих сторон знать об этом дополнительном контексте, используемом из среды.

Однако CompositionLocal не всегда является лучшим решением. Мы не рекомендуем злоупотреблять CompositionLocal, поскольку у этого подхода есть ряд недостатков:

CompositionLocal усложняет понимание поведения composable-функции. Поскольку они создают неявные зависимости, вызывающие объекты composable-функций, которые их используют, должны убедиться, что значение для каждого CompositionLocal удовлетворено.

Кроме того, может не быть четкого источника истины для этой зависимости, поскольку она может мутировать в любой части композиции. Поэтому отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перемещаться вверх по композиции, чтобы увидеть, где было предоставлено значение current. Такие инструменты, как Поиск использования в IDE или инспектор макетов Compose, предоставляют достаточно информации для устранения этой проблемы.

Решите, нужно ли использовать CompositionLocal

CompositionLocal – хороший вариант для вас, если:

Для CompositionLocal должно быть задано подходящее значение по умолчанию. Если значения по умолчанию нет, вы должны сделать так, чтобы разработчику было крайне сложно не указать значение для CompositionLocal. Если не указать значение по умолчанию, могут возникнуть проблемы при создании тестов или предварительном просмотре composable-функции, использующей CompositionLocal, поскольку его всегда нужно будет указывать явно.

Не используйте CompositionLocal для понятий, которые не считаются относящимися к дереву или поддереву. CompositionLocal имеет смысл, когда его потенциально может использовать любой потомок, а не несколько из них.

Если ваш сценарий использования не соответствует этим требованиям, ознакомьтесь с разделом Альтернативные варианты, прежде чем создавать CompositionLocal.

Примером неправильного подхода является создание CompositionLocal, который содержит ViewModel определенного экрана, чтобы все composable-функции на этом экране могли получить ссылку на ViewModel для выполнения некоторой логики. Это плохая практика, потому что не всем composable-функциям ниже определенного дерева пользовательского интерфейса нужно знать о ViewModel. Рекомендуется передавать в composable-функции только ту информацию, которая им нужна, следуя шаблону, согласно которому состояние передается вниз, а события – вверх. Такой подход позволит вам повторно использовать composable-функции и упростит тестирование.

Создать CompositionLocal

Создать CompositionLocal можно с помощью двух API:

  • compositionLocalOf. Изменение значения, предоставленного во время рекомпозиции, делает недействительным только контент, который считывает значение current.

  • staticCompositionLocalOf. В отличие от compositionLocalOf, чтение staticCompositionLocalOf не отслеживается Compose. Изменение значения приводит к рекомпозиции всей лямбда-функции content, в которой указан параметр CompositionLocal, а не только тех мест, где значение current считывается в композиции.

Если значение, передаваемое в CompositionLocal, вряд ли изменится или не изменится никогда, используйте staticCompositionLocalOf, чтобы повысить производительность.

Например, в системе проектирования приложения может быть указано, что composable-функции должны быть приподняты с помощью тени для компонента пользовательского интерфейса. Поскольку разные высоты для приложения должны распространяться по всему дереву пользовательского интерфейса, мы используем 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-функций, каждая из них должна содержать как можно меньше информации.

@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, чтобы получить те же преимущества:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}