O CompositionLocal
é uma ferramenta para
transmitir dados usando uma composição de forma implícita. Nesta página, você
vai aprender detalhes sobre o CompositionLocal
, como criar seu próprio CompositionLocal
e como saber se um CompositionLocal
é uma boa solução para
seu caso de uso.
Conheça o CompositionLocal
No Compose, geralmente os dados fluem para baixo na árvore da IU como parâmetros para cada função que pode ser composta. Dessa forma, as dependências da função que pode ser composta ficam explícitas. Contudo, isso pode ser complicado para dados que são usados em grande volume e frequência, como cores ou estilos de tipografia. Veja o exemplo a seguir:
@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 ) }
Para não precisar transmitir as cores como uma dependência de parâmetro explícita à
maioria dos elementos combináveis, o Compose fornece o CompositionLocal
, que permite
criar objetos nomeados com escopo de árvore que podem ser usados como uma forma implícita de fazer com que os
dados fluam pela árvore da IU.
Os elementos CompositionLocal
geralmente são fornecidos com um valor em um nó específico
da árvore da IU. Esse valor pode ser usado pelos descendentes compostos sem
declarar o CompositionLocal
como um parâmetro na função de composição.
O CompositionLocal
é usado internamente pelo tema do Material Design.
O MaterialTheme
é
um objeto que fornece três instâncias CompositionLocal
: colorScheme
,
typography
e shapes
, permitindo que você as recupere mais tarde em qualquer parte
descendente da composição.
Especificamente, essas são as propriedades LocalColorScheme
, LocalShapes
e
LocalTypography
que podem ser acessadas com os atributos MaterialTheme
colorScheme
, shapes
e 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 ) }
Uma instância CompositionLocal
tem como escopo uma parte da composição, para que
seja possível fornecer valores diferentes em níveis diferentes da árvore. O valor current
de um CompositionLocal
corresponde ao valor mais próximo fornecido por um
ancestral nessa parte da composição.
Para fornecer um novo valor a um CompositionLocal
, use o
CompositionLocalProvider
e a função de infixo provides
,
que associa uma chave do CompositionLocal
a um value
. A
lambda de content
do CompositionLocalProvider
vai receber o valor fornecido
ao acessar a propriedade current
do CompositionLocal
. Quando um
novo valor é fornecido, o Compose faz a recomposição de partes da composição que leem
o CompositionLocal
.
Como exemplo, a CompositionLocal
LocalContentColor
contém a cor de conteúdo preferencial usada para texto e
iconografia para garantir que tenha contraste com a cor de fundo atual. No
exemplo a seguir, o CompositionLocalProvider
é usado para fornecer valores diferentes
para partes distintas da composição.
@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") }
Figura 1. Visualização da função de composição CompositionLocalExample
.
No último exemplo, as instâncias CompositionLocal
eram usadas internamente
por elementos combináveis do Material Design. Para acessar o valor atual de um CompositionLocal
,
use a propriedade
current
. No exemplo a seguir, o valor atual do Context
do CompositionLocal
LocalContext
, usado com frequência em apps Android, é usado para formatar
o texto:
@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) }
Como criar seu próprio CompositionLocal
O CompositionLocal
é uma ferramenta para transmitir dados usando uma composição
de forma implícita.
Outro sinal importante para usar o CompositionLocal
é quando o parâmetro é
transversal e as camadas de implementação intermediárias não podem detectar
a existência desse parâmetro porque, caso o detectassem, a utilidade do elemento combinável
seria limitada. Por exemplo, a consulta de permissões do Android é
realizada internamente por um CompositionLocal
. Um elemento de seletor de mídia
pode adicionar novas funcionalidades para acessar conteúdos protegidos por alguma permissão no
dispositivo sem mudar a API nem exigir que os autores da chamada do seletor de mídia
identifiquem o contexto extra usado no ambiente.
No entanto, o CompositionLocal
nem sempre é a melhor solução. Não
recomendamos o uso excessivo do CompositionLocal
, porque ele tem algumas desvantagens:
O CompositionLocal
dificulta a compreensão do comportamento de funções que podem ser compostas. À
medida que criam dependências implícitas, os autores das chamadas dos elementos combináveis que usam essas dependências precisam
garantir que o valor de cada CompositionLocal
seja atendido.
Além disso, pode não existir uma fonte de verdade clara para essa dependência, já que ela
pode ser modificada em qualquer parte da composição. Dessa forma, pode ser mais difícil depurar o app quando ocorrer
um problema, já que seria preciso navegar
pela composição para identificar o local em que o valor current
foi fornecido. Ferramentas como Find
usages no ambiente de desenvolvimento integrado ou o Layout Inspector do Compose fornecem informações suficientes para
atenuar esse problema.
Como decidir se você quer usar CompositionLocal
Há algumas condições que podem fazer com que o CompositionLocal
seja uma boa solução
para seu caso de uso:
O CompositionLocal
precisa ter um bom valor padrão. Caso não exista um valor
padrão, é necessário garantir que as chances de um desenvolvedor se
deparar com uma situação em que nenhum valor é fornecido para CompositionLocal
sejam extremamente baixas.
Não informar um valor padrão pode causar problemas e frustração ao criar
testes ou fazer com que a visualização de uma função de composição que usa esse CompositionLocal
sempre
exija que ele seja fornecido de forma explícita.
Evite usar o CompositionLocal
para conceitos que não tenham a árvore ou
sub-hierarquia como escopo. Faz sentido implementar um CompositionLocal
quando ele pode ser
usado por todos os descendentes, e não apenas por alguns.
Se o caso de uso não atende a esses requisitos, consulte a seção
Alternativas a considerar antes de criar um
CompositionLocal
.
Um exemplo de prática não recomendada é criar um CompositionLocal
contendo o
ViewModel
de uma tela específica para que todos os elementos combináveis nessa tela possam
usar o ViewModel
como referência para executar uma determinada lógica. Essa é uma prática não recomendada,
porque nem todos os elementos combináveis abaixo de uma árvore da IU específica precisam ser compatíveis com um
ViewModel
. Uma prática recomendada é transmitir para as funções de composição somente as informações
de que elas precisam, seguindo o padrão estados fluem para baixo e eventos fluem para cima. Essa abordagem fará com que as funções de composição sejam mais
reutilizáveis e fáceis de testar.
Como criar um CompositionLocal
Existem duas APIs para criar um CompositionLocal
:
compositionLocalOf
: mudar o valor fornecido durante a recomposição invalida somente o conteúdo que lê o valorcurrent
.staticCompositionLocalOf
: diferente docompositionLocalOf
, as leituras de umstaticCompositionLocalOf
não são monitoradas pelo Compose. Mudar o valor faz com que toda a lambda decontent
em que oCompositionLocal
é fornecido seja recomposta, e não apenas os locais em que o valorcurrent
é lido na composição.
Caso seja muito improvável que o valor fornecido ao CompositionLocal
mude, ou
caso ele nunca mude, use staticCompositionLocalOf
para ter benefícios de desempenho.
Por exemplo, o sistema de design de um app pode ser rigoroso quanto à forma como as funções que podem ser compostas
são elevadas para o componente de IU usando uma sombra. Como as diferentes
elevações do app se propagam por toda a árvore da IU, nós usamos um
CompositionLocal
. Como o valor do CompositionLocal
é derivado de forma condicional
de acordo com o tema do sistema, usamos a 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() }
Como fornecer valores a um CompositionLocal
A função de composição CompositionLocalProvider
vincula valores a instâncias do CompositionLocal
para a hierarquia
fornecida. Para fornecer um novo valor a um CompositionLocal
, use
a função de infixo provides
,
que associa uma chave do CompositionLocal
a um value
desta maneira:
// 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 } } } }
Como consumir o CompositionLocal
O CompositionLocal.current
retorna o valor fornecido pelo CompositionLocalProvider
mais próximo, que fornece um valor para esse 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 } }
Alternativas a considerar
Implementar um CompositionLocal
pode ser uma solução exagerada para alguns casos de uso. Se o
caso de uso não atender aos critérios especificados na seção Como decidir se é necessário usar
um CompositionLocal, é possível que outra solução seja mais
adequada para esse caso.
Transmitir parâmetros explícitos
É importante definir as dependências das funções de composição de forma explícita. Recomendamos transmitir somente as informações necessárias às funções que podem ser compostas. Para incentivar o desacoplamento e a reutilização dos elementos combináveis, cada elemento precisa conter a menor quantidade de informações possível.
@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 }
Inversão de controle
Outra forma de evitar a transmissão de dependências desnecessárias para elementos combináveis é usar a inversão de controle. Em vez do descendente, é o pai que recebe uma dependência para executar uma determinada lógica.
Veja o exemplo a seguir em que um descendente precisa acionar a solicitação para carregar alguns dados:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
Dependendo do caso, o MyDescendant
pode ter muitas responsabilidades. Além disso,
transmitir o MyViewModel
como uma dependência faz com que o MyDescendant
seja menos reutilizável, já que
acopla ambos. Considere a alternativa que não transmite a
dependência ao descendente e usa os princípios da inversão de controle,
tornando o ancestral responsável pela execução da lógica:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Essa abordagem pode ser mais adequada para alguns casos de uso porque desacopla o elemento filho dos ancestrais imediatos. Os elementos combináveis ancestrais costumam se tornar mais complexos, o que favorece a existência de elementos mais flexíveis de nível mais baixo.
Do mesmo modo, lambdas de conteúdo @Composable
podem ser usadas desta forma
para ter os mesmos benefícios:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Anatomia de um tema no Compose
- Como usar visualizações no Compose
- Kotlin para Jetpack Compose