Dados com escopo local usando o CompositionLocal

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 de CompositionLocal (cores, tipografia e formas), permitindo que você as recupere posteriormente em qualquer parte descendente da composição. Mais especificamente, elas são as propriedades LocalColors, LocalShapes e LocalTypography que podem ser acessadas pelos atributos colors, shapes e typography do MaterialTheme.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.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, o CompositionLocal LocalContentAlpha contém o conteúdo Alfa recomendado para texto e iconografia para destacar ou remover o destaque de diferentes partes da IU. No exemplo a seguir, o CompositionLocalProvider é usado para fornecer valores diferentes para partes distintas da composição.

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

Figura 1. Visualização da função de composição CompositionLocalExample.

Em todos os exemplos acima, 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)
}

Crie 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 valor current.

  • staticCompositionLocalOf: diferente do compositionLocalOf, as leituras de um staticCompositionLocalOf não são monitoradas pelo Compose. Mudar o valor faz com que toda a lambda de content em que o CompositionLocal é fornecido seja recomposta, e não apenas os locais em que o valor current é 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
    Card(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()
    }
}