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.
Introducción a CompositionLocal
Por lo general, en Compose, los datos fluyen hacia abajo por el árbol de la IU como parámetros para cada función de componibilidad. Esto hace que las dependencias de un elemento componible sean explícitas. Sin embargo, esto puede ser engorroso para los datos que se usan con mucha frecuencia y de forma generalizada, como los colores o los estilos de tipografía. 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 que no sea necesario pasar los colores como una dependencia de parámetro explícita a la mayoría de los elementos componibles, Compose ofrece CompositionLocal, que te permite crear objetos con nombre con alcance de árbol que se pueden usar como una forma implícita de hacer que los datos fluyan por el árbol de la 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.
Términos clave: En esta guía, usamos los términos composición, árbol de IU y jerarquía de IU. Si bien se pueden usar de forma intercambiable en otras guías, tienen significados diferentes:
- La composición es el registro del gráfico de llamadas de las funciones que admiten composición.
- El árbol de IU o la jerarquía de IU es el árbol de
LayoutNodeque se construye, actualiza y mantiene mediante el proceso de composición.
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 adelante en cualquier parte descendiente de la composición.
Específicamente, estas son las propiedades LocalColorScheme, LocalShapes y LocalTypography a las que puedes acceder a través de los atributos MaterialTheme, colorScheme, shapes y 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 ) }
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.
Por ejemplo, el CompositionLocal de LocalContentColor contiene 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") }
CompositionLocalExampleEn el último ejemplo, las instancias de CompositionLocal se usaron de forma interna en los elementos componibles de Material. Para acceder al valor actual de un elemento CompositionLocal, usa su propiedad current. En el siguiente ejemplo, el valor actual de Context de LocalContext CompositionLocal que se usa de uso frecuente en las apps para Android se utiliza para dar 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) }
Crea tu propio CompositionLocal
CompositionLocal es una herramienta para pasar datos a través de la composición de forma implícita.
Otra señal clave para usar CompositionLocal es cuando el parámetro es transversal y las capas intermedias de implementación no deben saber que existe, ya que, si lo supieran, se limitaría la utilidad del elemento componible. 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 proporcionan suficiente información para mitigar este problema.
Decide si usarás 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. La práctica recomendada es pasar a los elementos componibles solo la información que necesitan siguiendo el patrón de que el estado fluye hacia abajo y los eventos fluyen hacia arriba.
Este enfoque hará que tus elementos componibles sean más reutilizables y fáciles de probar.
Cómo crear un CompositionLocal
Existen dos API para crear un elemento CompositionLocal:
compositionLocalOf: Cambiar el valor proporcionado 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 lambdacontenten la que se proporcionaCompositionLocal, en lugar de solo los lugares en los que se lee el valorcurrenten 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() }
Proporciona 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 el CompositionLocal
CompositionLocal.current devuelve el valor que proporciona el CompositionLocalProvider más cercano que proporciona un valor a ese 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 forma de evitar pasar dependencias innecesarias a un elemento componible es usar la inversión de control. En lugar de que el descendiente tome una dependencia para ejecutar cierta 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.
Del mismo modo, las lambdas de contenido @Composable se pueden usar de la misma manera para obtener los mismos 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