CompositionLocal
es una herramienta que permite pasar datos de manera implícita mediante la composición. En esta página, aprenderás con más detalle qué es CompositionLocal
y cómo crear tu propio elemento CompositionLocal
, y sabrás si CompositionLocal
es una buena solución para tu caso de uso.
Presentamos CompositionLocal
Por lo general, en Compose, los datos fluyen hacia abajo a través del árbol de IU como parámetros para cada función que admite composición. De esta manera, se logra que las dependencias de un elemento que admite composición sean explícitas. Sin embargo, esto puede ser complicado para los datos que se usan con mucha frecuencia, como los colores o los estilos de tipo. Observa el siguiente ejemplo:
@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 lograr que no se necesite pasar los colores como una dependencia explícita de parámetros a la mayoría de los elementos que admiten composición, Compose ofrece CompositionLocal
, que te permite crear objetos que tengan un nombre y un alcance de árbol, y que se puedan usar como una manera implícita para que los datos fluyan a través del árbol de IU.
En general, los elementos CompositionLocal
se aprovisionan con un valor en un nodo determinado del árbol de IU. Sus elementos subordinados que admiten composición pueden utilizar ese valor sin declarar CompositionLocal
como parámetro en la función de componibilidad.
CompositionLocal
es lo que usa MaterialTheme de forma interna.
MaterialTheme
es un objeto que proporciona tres instancias de CompositionLocal
: colorScheme
, typography
y shapes
, lo que te permite recuperarlas más tarde en cualquier parte descendiente de la composición.
En particular, estas son las propiedades LocalColorScheme
, LocalShapes
y LocalTypography
a las que tienes acceso mediante los atributos colorScheme
, shapes
y typography
de MaterialTheme
.
@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 ) }
Una instancia de CompositionLocal
tiene el alcance para una parte de la composición de manera que puedas brindar valores diferentes en distintos niveles del árbol. El valor current
de un elemento CompositionLocal
corresponde al valor más cercano que brinda un objeto principal en esa parte de la composición.
Para brindar un valor nuevo a un elemento CompositionLocal
, usa el objeto CompositionLocalProvider
y su función infija provides
, que asocia una clave CompositionLocal
a value
. La lambda content
del elemento CompositionLocalProvider
obtendrá el valor proporcionado cuando acceda a la propiedad current
de CompositionLocal
. Cuando se brinda un valor nuevo, Compose recompone partes de la composición que lee el elemento CompositionLocal
.
A modo de ejemplo, el objeto LocalContentColor
de CompositionLocal
incluye el color de contenido preferido que se usa para el texto y la iconografía para garantizar que contraste con el color de fondo actual. En el siguiente ejemplo, CompositionLocalProvider
se usa a fin de proporcionar diferentes valores para distintas partes de la composición.
@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: Vista previa del elemento CompositionLocalExample
que admite composición
En el último ejemplo, los elementos de Material que admiten composición usan de manera interna las instancias de CompositionLocal
. Para acceder al valor actual de un elemento CompositionLocal
, usa su propiedad current
. En el siguiente ejemplo, se utiliza el valor Context
actual del objeto LocalContext
CompositionLocal
que, por lo general, se usa en las apps para Android a fin de darle formato al 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) }
Cómo crear tu propio CompositionLocal
CompositionLocal
es una herramienta que permite pasar datos de manera implícita mediante la composición.
Otro indicador clave para usar CompositionLocal
es cuando el parámetro es transversal y las capas intermedias de implementación no deberían estar al tanto de su existencia, ya que, si estuvieran al tanto, se limitaría la utilidad del elemento que admite composición. Por ejemplo, la consulta de permisos de Android se otorga mediante un elemento CompositionLocal
interno. Un selector de medios que admite composición puede agregar funcionalidades nuevas para acceder al contenido protegido por permisos en el dispositivo sin cambiar su API ni solicitarles a los llamadores del selector de medios que estén al tanto de este contexto adicional que se usa desde el entorno.
Sin embargo, CompositionLocal
no siempre es la mejor solución. No te recomendamos que uses CompositionLocal
de manera excesiva, ya que tiene algunas desventajas:
CompositionLocal
causa que sea más difícil comprender el comportamiento de un elemento que admite composición. Como crean dependencias implícitas, los llamadores de elementos que admiten composición que las usan necesitan asegurarse de que se cumpla un valor para cada CompositionLocal
.
Además, es posible que no exista una fuente de información clara para esta dependencia, ya que puede mutar en cualquier parte de la composición. Por lo tanto, puede ser más desafiante depurar la app cuando se produce un problema, ya que debes navegar hasta la composición para verificar dónde se brindó el valor current
. Las herramientas como Find usages en el IDE o el Inspector de diseño de Compose brindan suficiente información para mitigar este problema.
Cómo decidir si usar CompositionLocal
Si se cumplen ciertas condiciones, CompositionLocal
puede ser una buena solución para tu caso de uso:
Un elemento CompositionLocal
debe tener un buen valor predeterminado. Si no existe un valor predeterminado, debes garantizar que, para un desarrollador, sea muy difícil intervenir en una situación en la que no se brinde un valor para CompositionLocal
.
No proporcionar un valor predeterminado puede causar problemas y generar frustración cuando se crean pruebas, u obtener una vista previa de un objeto que admite composición que usa ese elemento CompositionLocal
siempre exigirá que se brinde de forma explícita.
Evita CompositionLocal
para los conceptos sobre los cuales no se considera que tengan un alcance de árbol o de subjerarquía. Un objeto CompositionLocal
tiene sentido cuando cualquier elemento subordinado (no solo unos pocos) puede usarlo.
Si tu caso de uso no cumple con estos requisitos, consulta la sección Alternativas que debes tener en cuenta antes de crear un elemento CompositionLocal
.
Por ejemplo, no te recomendamos que crees un elemento CompositionLocal
que contenga el ViewModel
de una pantalla específica para que todos los elementos que admitan composición en esa pantalla puedan obtener una referencia al ViewModel
a fin de realizar alguna lógica. Se trata de una práctica no recomendada, ya que no todos los elementos que admiten composición debajo de un árbol de IU determinado deben estar al tanto de la existencia de un ViewModel
. Te recomendamos que pases a los elementos que admiten composición solo la información que necesiten según el patrón que indica que el estado fluye hacia abajo y los eventos fluyen hacia arriba. Con este enfoque, los elementos que admiten composición se podrán volver a utilizar más, y será más fácil probarlos.
Cómo crear un CompositionLocal
Existen dos API para crear un elemento CompositionLocal
:
compositionLocalOf
: Cambiar el valor que se brinda durante la recomposición invalida solo el contenido que lee su valorcurrent
.staticCompositionLocalOf
: A diferencia decompositionLocalOf
, Compose no realiza un seguimiento de las lecturas destaticCompositionLocalOf
. Cambiar el valor causa que se recomponga toda la lambdacontent
en la que se proporcionaCompositionLocal
, en lugar de solo los lugares en los que se lee el valorcurrent
en la composición.
Si es poco probable que cambie el valor que se brinda al elemento CompositionLocal
, o si nunca cambia, usa staticCompositionLocalOf
para obtener beneficios de rendimiento.
Por ejemplo, es posible que el sistema de diseño de una app se defina en la manera en que los elementos que admiten composición se elevan mediante una sombra para el componente de IU. Como las diferentes elevaciones de la app deben propagarse por el árbol de IU, usamos un elemento CompositionLocal
. Como el valor CompositionLocal
se deriva de manera condicional en función del tema del sistema, usamos la API de 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() }
Cómo proporcionar valores a un CompositionLocal
El elemento CompositionLocalProvider
que admite composición vincula los valores con las instancias de CompositionLocal
para la jerarquía determinada. Para brindar un valor nuevo a un elemento CompositionLocal
, usa la función infija provides
que asocia una clave CompositionLocal
a value
de la siguiente manera:
// 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 } } } }
Cómo consumir CompositionLocal
CompositionLocal.current
muestra el valor que brinda el objeto CompositionLocalProvider
más cercano que proporciona un valor a ese elemento 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 que debes tener en cuenta
Un elemento CompositionLocal
puede ser una solución exagerada para algunos casos de uso. Si tu caso de uso no cumple con los criterios que se especifican en la sección Cómo decidir si usar CompositionLocal, es probable que otra solución sea más adecuada para este.
Cómo pasar parámetros explícitos
Es una buena idea ser explícito acerca de las dependencias que admiten composición. Te recomendamos que pases a los elementos que admiten composición solo lo que necesiten. Para promover la separación y la reutilización de elementos que admiten composición, cada uno debe incluir la menor cantidad de información posible.
@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 }
Inversión de control
Otra manera de evitar pasar dependencias innecesarias a un elemento que admite composición es mediante la inversión de control. En lugar de que el elemento subordinado tome una dependencia para ejecutar alguna lógica, el elemento superior lo hace.
Observa el siguiente ejemplo, en el que un elemento subordinado debe activar la solicitud para cargar algunos datos:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
Según el caso, es posible que MyDescendant
tenga mucha responsabilidad. Además, pasar MyViewModel
como una dependencia causa que MyDescendant
se pueda volver a utilizar menos, ya que ahora están vinculados. Ten en cuenta la alternativa que no pasa la dependencia al elemento subordinado y recurre a los principios de la inversión de control que causan que el elemento principal sea responsable de ejecutar la lógica:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
Este enfoque es más adecuado para algunos casos de uso, ya que separa el elemento secundario de sus elementos principales inmediatos. Los elementos principales que admiten composición suelen ser más complejos a cambio de tener elementos más flexibles que admiten composición de nivel inferior.
De manera similar, se pueden usar lambdas de contenido @Composable
del mismo modo para obtener estos beneficios:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Anatomía de un tema en Compose
- Cómo usar objetos View en Compose
- Kotlin para Jetpack Compose