Данные локального масштаба с помощью 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 ограничен частью композиции , поэтому вы можете предоставлять разные значения на разных уровнях дерева. 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")
}

Рисунок 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 — это инструмент для неявной передачи данных через Composition .

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

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

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

Более того, для этой зависимости может не быть четкого источника истины, поскольку она может мутировать в любой части композиции. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перейти вверх по композиции, чтобы увидеть, где было предоставлено current значение. Такие инструменты, как « Найти использование в среде IDE» или «Инспектор компоновки компоновки», предоставляют достаточно информации для устранения этой проблемы.

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

Существуют определенные условия, которые могут сделать CompositionLocal хорошим решением для вашего варианта использования:

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

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

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

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

Создание CompositionLocal

Существует два API для создания CompositionLocal :

  • 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

Составной компонент 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()
    }
}

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

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

Рисунок 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 — это инструмент для неявной передачи данных через Composition .

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

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

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

Более того, для этой зависимости может не быть четкого источника истины, поскольку она может мутировать в любой части композиции. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перейти вверх по композиции, чтобы увидеть, где было предоставлено current значение. Такие инструменты, как « Найти использование в среде IDE» или «Инспектор компоновки компоновки», предоставляют достаточно информации для решения этой проблемы.

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

Существуют определенные условия, которые могут сделать CompositionLocal хорошим решением для вашего варианта использования:

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

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

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

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

Создание CompositionLocal

Существует два API для создания CompositionLocal :

  • 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

Составной компонент 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()
    }
}

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

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

Рисунок 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 — это инструмент для неявной передачи данных через Composition .

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

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

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

Более того, для этой зависимости может не быть четкого источника истины, поскольку она может мутировать в любой части композиции. Таким образом, отладка приложения при возникновении проблемы может быть более сложной, поскольку вам нужно перейти вверх по композиции, чтобы увидеть, где было предоставлено current значение. Такие инструменты, как « Найти использование в среде IDE» или «Инспектор компоновки компоновки», предоставляют достаточно информации для устранения этой проблемы.

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

Существуют определенные условия, которые могут сделать CompositionLocal хорошим решением для вашего варианта использования:

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

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

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

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

Создание CompositionLocal

Существует два API для создания CompositionLocal :

  • 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

Составной компонент 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()
    }
}

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

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

Рисунок 1. Предварительный просмотр компонуемого объекта CompositionLocalExample .

В последнем примере экземпляры CompositionLocal использовались для внутреннего использования компонуемыми материалами. Чтобы получить доступ к текущему значению 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 удовлетворяется.

Кроме того, не может быть четкого источника истины для этой зависимости, поскольку она может мутировать в любой части композиции. Таким образом, отладка приложения, когда возникает проблема, может быть более сложной, поскольку вам нужно ориентироваться в композиции, чтобы увидеть, где было предоставлено 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 Lambda предоставляется CompositionLocal , который должен быть отменен, а не только места, где current значение читается в композиции.

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

Например, система проектирования приложения может быть самоуверенна в том, как композиционные устройства повышаются с использованием тени для компонента пользовательского интерфейса. Поскольку различные возвышения для приложения должны распространяться по всему дереву пользовательского интерфейса, мы используем CompositionLocal . Поскольку CompositionLocal значение получено условно на основе темы системы, мы используем compositionLocalOf API:

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

Передайте явные параметры

Быть явным в отношении зависимости композиции - хорошая привычка. Мы рекомендуем вам пройти только то, что им нужно . Чтобы поощрять развязку и повторное использование композиционных товаров, каждый композитный должен содержать наименее возможным объемом информации.

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

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

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

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