Данные локального масштаба с помощью CompositionLocal

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()
    }
}

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}