Modificadores de Compose

Los modificadores te permiten decorar o aumentar un elemento componible. Por ejemplo, puedes hacer todo esto:

  • Cambiar el tamaño, el diseño, el comportamiento y el aspecto del elemento componible
  • Agregar información (p. ej., etiquetas de accesibilidad)
  • Procesar entradas del usuario
  • Agregar interacciones de nivel superior, (p. ej., hacer que un elemento sea apto para hacer clic, desplazable, arrastrable o ampliable)

Los modificadores son objetos estándar de Kotlin. Para crear uno, llama a una de las funciones de clase Modifier.

@Composable
private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

Dos líneas de texto sobre un fondo de color con padding alrededor del texto

Puedes encadenar estas funciones para crear una composición:

@Composable
private fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxWidth()
    ) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

El fondo de color detrás del texto ahora extiende todo el ancho del dispositivo.

En el código anterior, observa distintas funciones de modificadores que se usan juntas.

  • padding coloca espacio alrededor de un elemento.
  • fillMaxWidth hace que el elemento componible ocupe el ancho máximo que le otorga su elemento superior.

La práctica recomendada es que todos los elementos componibles acepten un parámetro modifier y pasen ese modificador al primer elemento secundario que emita la IU. Si lo haces, tu código será más reutilizable y su comportamiento será más intuitivo y predecible. Para obtener más información, consulta los lineamientos de la API de Compose Cómo aceptar y respetar elementos de un parámetro modificador.

El orden de los modificadores es importante

El orden de las funciones de los modificadores es importante. Como cada función realiza cambios en el Modifier que muestra la función anterior, la secuencia afecta al resultado final. Veamos un ejemplo:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

Toda el área, incluso el padding alrededor de los bordes, responde a los clics

En el código anterior, se puede hacer clic en toda el área, incluso en el padding que la rodea, porque se aplicó el modificador padding después del modificador clickable. Si se invierte el orden de los modificadores, el espacio que agrega padding no reacciona a la entrada del usuario:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

El padding alrededor del borde del diseño ya no responde a los clics

Modificadores integrados

Jetpack Compose proporciona una lista de modificadores integrados para ayudarte a decorar o aumentar un elemento componible. A continuación, se muestran algunos modificadores comunes que usarás para ajustar tus diseños.

padding y size

De forma predeterminada, los diseños proporcionados en Compose unen sus objetos secundarios. Sin embargo, puedes establecer un tamaño con el modificador size:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

Ten en cuenta que el tamaño especificado podría no respetarse si no cumple con las restricciones provenientes del elemento superior del diseño. Si necesitas que el tamaño del elemento componible se corrija independientemente de las restricciones entrantes, usa el modificador requiredSize:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

La imagen secundaria es más grande que las restricciones que provienen de su elemento superior

En este ejemplo, incluso con el height superior establecido en 100.dp, la altura de la Image será 150.dp, ya que el modificador requiredSize tiene prioridad.

Si quieres que un diseño secundario rellene todo el alto disponible que permite el elemento superior, agrega el modificador fillMaxHeight (Compose también proporciona fillMaxSize y fillMaxWidth):

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

La altura de la imagen es tan grande como la del elemento superior

Para agregar padding alrededor de un elemento, establece un modificador padding.

Si deseas agregar padding sobre un modelo de referencia de texto para alcanzar una distancia específica desde la parte superior del diseño al modelo de referencia, usa el modificador paddingFromBaseline:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

Texto con padding encima

Desplazamiento

Para posicionar un diseño en relación con su posición original, agrega el modificador offset y configura el desplazamiento en los ejes x y y. Las compensaciones pueden ser positivas y no positivas. La diferencia entre padding y offset es que agregar un offset a un elemento componible no cambia sus mediciones:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

El texto se movió al lado derecho del contenedor superior

El modificador offset se aplica horizontalmente según la dirección del diseño. En un contexto de izquierda a derecha, un offset positivo cambia el elemento hacia la derecha, mientras que en un contexto de derecha a izquierda, desplaza el elemento hacia la izquierda. Si necesitas establecer un desplazamiento sin considerar la dirección del diseño, consulta el modificador absoluteOffset, en el que un valor de desplazamiento positivo siempre cambia el elemento hacia la derecha.

El modificador offset proporciona dos sobrecargas: offset, que toma las compensaciones como parámetros, y offset, que toma en una lambda. Para obtener información más detallada sobre cuándo usar cada una de ellas y cómo optimizar el rendimiento, lee la sección Rendimiento de Compose: aplazar las lecturas el mayor tiempo posible.

Seguridad del alcance en Compose

En Compose, existen modificadores que solo se pueden usar cuando se aplican a elementos secundarios de determinados elementos componibles. Compose aplica esto mediante alcances personalizados.

Por ejemplo, si deseas que un elemento secundario sea tan grande como el elemento Box superior sin afectar el tamaño de Box, usa el modificador matchParentSize. matchParentSize solo está disponible en BoxScope. Por lo tanto, solo se puede usar en un elemento secundario dentro de un elemento superior Box.

La seguridad del alcance te impide agregar modificadores que no funcionarían en otros elementos componibles y permisos, y te ahorra tiempo de prueba y error.

Los modificadores con alcance notifican al elemento superior sobre cierta información del elemento secundario que el superior debe conocer. Por lo general, también se los conoce como modificadores de datos del elemento superior. Sus aspectos internos difieren de los modificadores de uso general, pero, desde una perspectiva de uso, estas diferencias no resultan importantes.

matchParentSize en Box

Como se mencionó con anterioridad, si deseas que un diseño secundario tenga el mismo tamaño que un elemento Box superior sin afectar el tamaño del Box, usa el modificador matchParentSize.

Ten en cuenta que matchParentSize solo está disponible dentro de un alcance de Box, lo que significa que solo se aplica a campos secundarios directos de elementos componibles Box.

En el siguiente ejemplo, el Spacer secundario toma su tamaño desde el elemento Box superior, que, a su vez, toma su tamaño del elemento secundario más grande, ArtistCard en este caso.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(
            Modifier
                .matchParentSize()
                .background(Color.LightGray)
        )
        ArtistCard()
    }
}

Fondo gris que rellena su contenedor

Si se usara fillMaxSize en lugar de matchParentSize, Spacer tomaría todo el espacio disponible para el elemento superior, lo que podría hacer que este se expanda y rellene todo el espacio disponible.

Fondo gris que ocupa toda la pantalla

weight en Row y Column

Como viste en la sección anterior Padding y tamaño, el tamaño de un objeto componible se define de forma predeterminada según el contenido al que está unido. Puedes configurar el tamaño de un elemento componible para que sea flexible dentro de su elemento superior usando el modificador weight que solo está disponible en RowScope y ColumnScope.

Tomemos un objeto Row que contiene dos elementos componibles Box. El primer cuadro recibe el doble de weight del segundo, por lo que recibe el doble del ancho. Dado que Row tiene 210.dp de ancho, el primer elemento Box tendrá 140.dp de ancho, y el segundo, 70.dp:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

El ancho de la imagen es el doble del ancho del texto

Extraer y reutilizar modificadores

Se pueden encadenar varios modificadores para decorar o aumentar un elemento componible. Esta cadena se crea a través de la interfaz Modifier, que representa una lista inmutable y ordenada de una sola Modifier.Elements.

Cada Modifier.Element representa un comportamiento individual, como los comportamientos de diseño, dibujo y gráficos, todos los comportamientos de gestos, enfoque y semántica, así como los eventos de entrada de dispositivos. El orden es importante: los elementos modificadores que se agreguen primero se aplicarán primero.

A veces, puede ser beneficioso reutilizar las mismas instancias de cadena de modificadores en varios elementos componibles al extraerlos en variables y elevarlos a alcances más altos. Esto puede mejorar la legibilidad del código o mejorar el rendimiento de tu app por varios motivos:

  • La reasignación de los modificadores no se repetirá cuando se produzca la recomposición de elementos componibles que los usan
  • Las cadenas modificadoras pueden ser muy largas y complejas, por lo que reutilizar la misma instancia de una cadena puede aliviar la carga de trabajo que necesita el entorno de ejecución de Compose cuando se comparan.
  • Esta extracción promueve la limpieza, la coherencia y el mantenimiento del código en su base

Prácticas recomendadas para reutilizar modificadores

Crea tus propias cadenas de Modifier y extráelas para reutilizarlas en varios componentes componibles. Está bien guardar un modificador, ya que son objetos similares a los datos:

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

Extraer y reutilizar modificadores cuando se observa un estado que cambia con frecuencia

Cuando se observan estados que cambian con frecuencia dentro de elementos componibles, como estados de animación o scrollState, puede haber una cantidad significativa de recomposiciones. En este caso, tus modificadores se asignarán en cada recomposición y, posiblemente, en cada fotograma:

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // Creation and allocation of this modifier will happen on every frame of the animation!
        modifier = Modifier
            .padding(12.dp)
            .background(Color.Gray),
        animatedState = animatedState
    )
}

En su lugar, puedes crear, extraer y reutilizar la misma instancia del modificador y pasarla al elemento componible de la siguiente manera:

// Now, the allocation of the modifier happens here:
val reusableModifier = Modifier
    .padding(12.dp)
    .background(Color.Gray)

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // No allocation, as we're just reusing the same instance
        modifier = reusableModifier,
        animatedState = animatedState
    )
}

Extraer y reutilizar modificadores sin alcance

Los modificadores pueden tener o no alcance a un elemento componible específico. En el caso de los modificadores sin alcance, puedes extraerlos fácilmente fuera de cualquier elemento componible como variables simples:

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

@Composable
fun AuthorField() {
    HeaderText(
        // ...
        modifier = reusableModifier
    )
    SubtitleText(
        // ...
        modifier = reusableModifier
    )
}

Esto puede ser especialmente beneficioso cuando se combina con diseños diferidos. En la mayoría de los casos, querrás que todos los elementos potencialmente significativos tengan los mismos modificadores:

val reusableItemModifier = Modifier
    .padding(bottom = 12.dp)
    .size(216.dp)
    .clip(CircleShape)

@Composable
private fun AuthorList(authors: List<Author>) {
    LazyColumn {
        items(authors) {
            AsyncImage(
                // ...
                modifier = reusableItemModifier,
            )
        }
    }
}

Extraer y reutilizar modificadores con alcance

Cuando se trata de modificadores con alcance a ciertos elementos componibles, puedes extraerlos al nivel más alto posible y volver a usarlos cuando corresponda:

Column(/*...*/) {
    val reusableItemModifier = Modifier
        .padding(bottom = 12.dp)
        // Align Modifier.Element requires a ColumnScope
        .align(Alignment.CenterHorizontally)
        .weight(1f)
    Text1(
        modifier = reusableItemModifier,
        // ...
    )
    Text2(
        modifier = reusableItemModifier
        // ...
    )
    // ...
}

Solo debes pasar los modificadores extraídos con alcance a los elementos secundarios directos de igual alcance. Consulta la sección Seguridad del alcance en Compose para obtener más información sobre por qué esto es importante:

Column(modifier = Modifier.fillMaxWidth()) {
    // Weight modifier is scoped to the Column composable
    val reusableItemModifier = Modifier.weight(1f)

    // Weight will be properly assigned here since this Text is a direct child of Column
    Text1(
        modifier = reusableItemModifier
        // ...
    )

    Box {
        Text2(
            // Weight won't do anything here since the Text composable is not a direct child of Column
            modifier = reusableItemModifier
            // ...
        )
    }
}

Encadenamiento adicional de los modificadores extraídos

Puedes encadenar o agregar aún más las cadenas de modificadores extraídas mediante una llamada a la función .then():

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

// Append to your reusableModifier
reusableModifier.clickable { /*...*/ }

// Append your reusableModifier
otherModifier.then(reusableModifier)

Recuerda que el orden de los modificadores es importante.

Más información

Proporcionamos una lista completa de modificadores, con sus parámetros y alcances.

Para obtener más información sobre cómo usar modificadores, puedes consultar los diseños básicos en el codelab de Compose o consultar el repositorio Now in Android.

Para más información sobre los modificadores personalizados y cómo crearlos, consulta la documentación sobre Diseños personalizados: Cómo usar el modificador de diseño.