Tutorial De Botones En Compose

En este tutorial, aprenderás acerca del uso de botones en Compose. Los botones le permiten a los usuarios realizar acciones y tomar decisiones con un tap en su superficie. Verás que en el Material Design existen cuatro tipos de botones y múltiples opciones de configuración de su contenido.

Antes de comenzar, si eres nuevo en Compose, te recomiendo leer:

Con esto en claro, te dejo una tabla con todos los temas que veremos del componente Button:

Ejemplo De Botones En Compose

Puedes encontrar los ejemplos de código de botones en Compose en el paquete p7_componentes del repositorio en GitHub.

Ejemplo de botones en Compose

Al interior del paquete examples/Button encontrarás un archivo Kotlin por cada sección descrita en este tutorial. El archivo ButtonsScreen.kt contiene la función componible donde puedes invocar el ejemplo que desees probar o una previsualización de dicho contenido.


1. Tipos De Botones

Text Button

Usa un text button para expresar poco énfasis de una acción, es decir, acciones cuyo pronunciamiento es menor debido a la superficie en que se encuentra (comúnmente cards y dialogs) o a su baja frecuencia de uso. Un text button no tiene contenedor, ya que no es su fin distraer al usuario del contenido cercano.

Apariencia de TextButton en Compose

En Compose este tipo de botones son representados por la función de alto nivel TextButton(), la cual recibe los siguientes parámetros:

@Composable
fun TextButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = null,
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.textButtonColors(),
    contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
    content: RowScope.() -> Unit
): @Composable Unit

Ejemplo:

Crear un botón de texto que cuando sea presionado actualice su texto con la cantidad de veces que ha ocurrido este evento.

@Composable
fun TextButtonExample() {
    var counter by remember { mutableStateOf(0) }

    TextButton(onClick = { counter++ }) {
        Text("CLICS: $counter")
    }
}

Los atributos principales de la configuración serán content para definir el contenido, onClick para controlar los eventos de clic y modifier para redimensionar, posicionar al botón.

El ejemplo anterior guarda el estado del contador en counter, el cual es actualizado en onClick y se refleja en el texto del componente Text.

Outlined Button

El outlined button (botón delineado) representa una acción con un énfasis medio, lo que quiere decir que esta es importante, pero no lo suficiente para tratarse de una acción principal.

Apariencia de OutlinedButton en Compose

Para crear uno, invoca a la función OutlinedButton(). Sus parámetros son idénticos a los de TextButton, por lo que procedes con el mismo criterio.

Ejemplo:

Crear un OutlinedButton que al ser cliqueado muestre en un Text la fecha y hora actual.

@Composable
fun OutlinedButtonExample() {
    var date by remember { mutableStateOf(LocalDateTime.now()) }

    Column {

        OutlinedButton(
            onClick = { date = LocalDateTime.now() },
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Text("INFORMAR")
        }
        Spacer(Modifier.size(16.dp))
        Text(
            "Fecha actual -> $date",
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
    }
}
Ejemplo de OutlinedButton en Compose

Contained Button

El contained button representa alto énfasis para las acciones primarias en tu App. Visualmente tienen elevación y su contenedor está relleno por un color sólido, de esta forma logran destacar de otros elementos alrededor.

Apariencia contained button en Compose

Inclúyelos en tu UI a través de la función Button(), que a su vez posee los mismos parámetros que los dos tipos de botones anteriores.

Ejemplo:

Crear un Contained Button que cambie sus dimensiones entre 150, 200 y 300dp cada que es presionado:

@Composable
fun ContainedButtonExample() {
    val WIDTH1 = 150.dp
    val WIDTH2 = 200.dp
    val WIDTH3 = 300.dp

    var width by remember { mutableStateOf(WIDTH1) }

    Button(
        onClick = {
            width = when (width) {
                WIDTH1 -> WIDTH2
                WIDTH2 -> WIDTH3
                else -> WIDTH1
            }
        },
        modifier = Modifier.width(width)
    ) {
        Text("CAMBIAR")
    }
}
Ejemplo Contained Button en Compose

Toggle Button

Los iconos pueden actuar como toggle buttons en el momento que desees presentar una sola opción seleccionable.

Ejemplo IconToggleButton Compose

Para ello compose provee el componente IconToggleButton, que te permite crear un ejemplar con los siguientes parámetros:

@Composable
fun IconToggleButton(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: () -> Unit
): @Composable Unit

Donde checked determina si el toggle button está marcado y onCheckedChange te permite especificar las acciones a realizar cuando sea seleccionado.

Ejemplo:

El icono de la ilustración anterior se crea con la siguiente función componible:

@Composable
fun ToggleButtonExample() {
    var checked by remember { mutableStateOf(false) } //1

    IconToggleButton(checked = checked, onCheckedChange = { checked = it }) { //2
        Icon(
            painter = painterResource( //3
                if (checked) R.drawable.ic_bookmark
                else R.drawable.ic_bookmark_border
            ),
            contentDescription = //4
            if (checked) "Añadir a marcadores"
            else "Quitar de marcadores",
            tint = Color(0xFF26C6DA) //5
        )
    }
}

La implementación se basa en:

  1. Declarar un estado booleano que recuerde la activación/desactivación del toggle button
  2. Crear el elemento IconToggleButton pasándole la variable checked en el parámetro del mismo nombre y actualizando dicho estado desde onCheckedChange
  3. Dibujar un componente Icon que usa a checked para determinar el recurso drawable a pintar
  4. La descripción del contenido también cambiará el texto de asistencia según el estado
  5. El color del icono será Blue 400

IconButton

El componente IconButton te permite crear iconos cliqueables, para representar acciones en otros componentes. Su tamaño es de 48x48dp y el icono en su interior debería ser de 24x24dp para mantener los lineamientos del Material Design.

Apariencia IconButton Compose

Sus parámetros son simples:

@Composable
fun IconButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: () -> Unit
): @Composable Unit

El valor de content típicamente es un elemento Icon que especifique la imagen a dibujar en el contenido.

Ejemplo:

Aplicar un tinte aleatorio cuando un IconButton es presionado.

@Composable
fun IconButtonExample() {
    var color by remember { mutableStateOf(Color.LightGray) }
    
    IconButton(
        onClick = {
            val randomColor = Color(Random.nextLong(0xFF000000, 0xFFFFFFFF))
            color = randomColor
        }) {
        Icon(
            Icons.Filled.Home,
            contentDescription = "Cambiar color",
            tint = color
        )
    }
}
Ejemplo IconButton Compose

2. Configuración De Botones

En esta sección veremos el uso de varios parámetros de los componentes de botón que actúan como opciones de configuración general.

Añadir icono

Si deseas comunicar el significado de la acción de un botón a través de un icono, entonces añade un elemento Icon antes de la etiqueta de texto como se muestra en el siguiente ejemplo:

Button(onClick = { }) {
    Icon(
        imageVector = Icons.Default.ShoppingCart,
        contentDescription = null,
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("BOTÓN")
}

Empleamos al objeto de utilidad ButtonDefaults para acceder a dos propiedades clave: IconSize para el tamaño estándar del icono dentro de un botón e IconSpacing para el correcto espaciado entre el icono y la etiqueta de texto.

Desactivar Un Botón

Si quieres comunicar que el botón no es interactivo y retirar todo su enfasis de la UI, entonces pasa el valor de false al parámetro enabled.

Desactivar botón Compose

Ejemplo:

Desactivar un botón de guardado cuando un campo de texto no tiene al menos tres caracteres.

@Composable
fun DisabledButtonExample() {
    var tag by remember { mutableStateOf("") }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        TextField(
            value = tag,
            onValueChange = { tag = it },
            label = { Text("Etiqueta") }
        )
        Spacer(Modifier.size(8.dp))
        Button(onClick = { }, enabled = tag.length > 2) {
            Text("GUARDAR")
        }
    }
}
Ejemplo enabled Button Compose

Como ves, determinamos el valor de desactivación en enabled. Su valor cambia a partir del condicional asociado al tamaño del String en el campo de texto.

Añadir Borde A Un Botón

Utiliza el atributo border de las funciones *Button para especificar un borde de tipo BorderStroke. Este recubrirá el contorno del botón según el tamaño y color que especifiques.

Ejemplo:

Aplicar un borde de 2dp de grosor y con un gradiente horizontal como color, a un botón con la etiqueta de texto «JUGAR»:

@Composable
fun BorderExample() {
    OutlinedButton(
        onClick = { }, border = BorderStroke(
            width = 2.dp,
            brush = Brush.horizontalGradient(
                listOf(
                    Color(0xFF42A5F5),
                    Color(0xFFFFA726)
                )
            )
        )
    ) {
        Text("JUGAR", color = Color(0xFF5C6BC0))
    }
}

3. Estilizar Botones

Cambiar Forma

Los botones hacen parte de la categoría de small components al momento de describir la figura de sus esquinas. Por esta razón, el parámetro shape de los componentes de botón usa como valor por defecto a MaterialTheme.shapes.small.

Personalizar la forma de un botón solo consiste en pasar una instancia del tipo BaseCornerShape.

Por ejemplo:

La siguiente función componible muestra varios tipos de formas aplicadas a las esquinas de un botón de inicio de sesión:

@Composable
fun ButtonShapeExample() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Formas Personalizadas", style = MaterialTheme.typography.h6)
        Spacer(Modifier.size(8.dp))
        Button(onClick = { }, shape = CutCornerShape(0)) {
            Text("BOTÓN")
        }
        Spacer(Modifier.size(8.dp))
        Button(onClick = { }, shape = CutCornerShape(4.dp)) {
            Text("BOTÓN")
        }
        Spacer(Modifier.size(8.dp))
        Button(onClick = { }, shape = RoundedCornerShape(50)) {
            Text("BOTÓN")
        }
    }
}
Ejemplo shape Button Compose

Cambiar Colores

De manera similar a TextField, las funciones de botones poseen al parámetro colors del tipo ButtonColors. Esta interfaz especifica los colores del fondo y contenido del botón.

Para producir instancias de ella usaremos las funciones textButtonColors(), outlinedButtonColors() y buttonColors() de ButtonDefaults.

Ejemplo:

Cambiar los colores de cada tipo de botón con valores de la paleta del Material Design.

@Composable
fun ColorsButtonExample() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Colores Personalizados", style = MaterialTheme.typography.h6)
        Spacer(Modifier.size(8.dp))
        TextButton(
            onClick = { },
            colors = ButtonDefaults.textButtonColors(
                contentColor = Color(0xFFEC407A)
            )
        ) {
            Text("TEXT BUTTON")
        }
        Spacer(Modifier.size(8.dp))
        OutlinedButton(
            onClick = { },
            colors = ButtonDefaults.outlinedButtonColors(
                contentColor = Color(0xFFAB47BC)
            )
        ) {
            Text("OUTLINED BUTTON")
        }
        Spacer(Modifier.size(8.dp))
        Button(
            onClick = { },
            colors = ButtonDefaults.buttonColors(
                backgroundColor = Color(0xFF7E57C2),
                contentColor = Color(0xFFFFEE58)
            )
        ) {
            Text("CONTAINED BUTTON")
        }
    }
}

backgroundColor corresponde al color del fondo del contenedor y contentColor al color del contenido (los componentes al interior de content).

Aunque también podrás asignar colores en el caso de la desactivación del botón con disabledBackgroundColor y disabledContentColor.


4. Grupo De Toggle Buttons

En el caso de que desees agrupar opciones relacionadas y sólo una pueda ser seleccionada, los toggle buttons pueden ser configurados en un grupo para resolver este problema. Estos se ubican en un mismo contenedor, donde la activación de uno de los botones, significa la desactivación de los demás.

Ejemplo Toggle Button Compose

En el sistema de views, la librería de materiales dispone de la clase MaterialButtonToggleGroup para representar un grupo de toggle buttons. Pero en Compose debes implementarlo con botones delineados.

Ejemplo:

Crear un grupo de toggle buttons que ofrezcan las opciones «AUTO», «OSCURO» y «CLARO» como se vio en la ilustración pasada:

@Composable
private fun ButtonToggleGroup( // 1
    options: List<String>,
    selectedOption: String,
    onOptionSelect: (String) -> Unit,
    modifier: Modifier = Modifier
) {

    Row(modifier = modifier) { // 2
        options.forEachIndexed { index, option -> // 3
            val selected = selectedOption == option // 4

            val border = if (selected) BorderStroke( // 5
                width = 1.dp,
                color = MaterialTheme.colors.primary
            ) else ButtonDefaults.outlinedBorder

            val shape = when (index) { // 6
                0 -> RoundedCornerShape(
                    topStart = 4.dp,
                    bottomStart = 4.dp,
                    topEnd = 0.dp,
                    bottomEnd = 0.dp
                )
                options.size - 1 -> RoundedCornerShape(
                    topStart = 0.dp, bottomStart = 0.dp,
                    topEnd = 4.dp,
                    bottomEnd = 4.dp
                )
                else -> CutCornerShape(0.dp)
            }

            val zIndex = if (selected) 1f else 0f

            val buttonModifier = when (index) { // 7
                0 -> Modifier.zIndex(zIndex)
                else -> {
                    val offset = -1 * index
                    Modifier
                        .offset(x = offset.dp)
                        .zIndex(zIndex)
                }
            }

            val colors = ButtonDefaults.outlinedButtonColors( // 8
                backgroundColor = if (selected) MaterialTheme.colors.primary.copy(alpha = 0.12f)
                else MaterialTheme.colors.surface,
                contentColor = if (selected) MaterialTheme.colors.primary else Color.DarkGray
            )
            OutlinedButton( // 9
                onClick = { onOptionSelect(option) },
                border = border,
                shape = shape,
                colors = colors,
                modifier = buttonModifier.weight(1f)
            ) {
                Text(option) // 10
            }
        }
    }
}

En el código anterior:

  1. Creamos la función componible con cuatro parámetros producto del state hoisting. Donde options es la lista de textos de las opciones, selectedOption la opción seleccionada, onOptionSelect el controlador cuando es seleccionada una opción y modifier el modificador para permitir reutilizar configuraciones del componente completo
  2. Usamos un layout Row para organizar los botones en fila y le pasamos el modificador entrante
  3. Iteramos sobre la lista de opciones con la función forEachIndexed()
  4. Verificamos si la opción de la iteración está seleccionada
  5. Desde aquí comenzamos a crear todos los parámetros para el botón. El primero será el borde, el cual cambia su color al primario si está seleccionado. De lo contrario usamos el borde por defecto outlinedBorder
  6. Creamos la forma del botón. Los botones interiores no tendrán esquinas afectadas, pero el primero y el último solo tendrán redondeo en las esquinas de los extremos
  7. Preparamos el modificador para eliminar el doble borde que se presentará entre dos botones contiguos. En este caso usamos a zIndex() para determinar el orden del dibujado. Quiere decir que un botón con mayor valor, será dibujado encima de uno con menor. Adicionalmente aplicamos offset() negativo en el eje X en todos los botones que no sean el primero
  8. Definimos los colores de selección y deselección de los botones
  9. Creamos el OutlinedButton y pasamos todos los parámetros producidos. Transmitimos el click sobre cada botón, invocando a onOptionSelect() con la opción actual
  10. El texto del botón será la opción actual

Finalmente, creamos un ButtonToggleButton desde otra función componible que maneje el estado:

@Composable
fun ButtonToggleGroupExample() {
    val auto = "AUTO"
    val dark = "OSCURO"
    val light = "LIGHT"
    val options = listOf(auto, dark, light)

    var selectedOption by remember { mutableStateOf(options[0]) }

    ButtonToggleGroup(
        options = options,
        selectedOption = selectedOption,
        onOptionSelect = { selectedOption = it },
    )
}

Grupo De Toggle Buttons Con Solo Iconos

En el caso que desees solo iconos en cada opción del grupo de toggle buttons, entonces reemplaza el componente Text del contenido en el OutlinedButton por un Icon.

Ejemplo Toggle Button Iconos Compose

Por ejemplo:

Cambiar la alineación de un texto a partir de un grupo de toggle buttons que especifican las siguientes acciones: Centrar, alinear a la izquierda y alinear a la derecha:

@Composable
private fun ButtonToggleGroup(
    options: List<Int>,
    selectedOption: Int,
    onOptionSelect: (Int) -> Unit,
    modifier: Modifier = Modifier
) {

    Row(modifier = modifier) {
        options.forEachIndexed { index, option ->
            //...

            OutlinedButton(
                onClick = { onOptionSelect(option) },
                border = border,
                shape = shape,
                colors = colors,
                contentPadding = PaddingValues(12.dp),
                modifier = buttonModifier.defaultMinSize(48.dp)
            ) {
                Icon(
                    painter = painterResource(option),
                    contentDescription = null
                )
            }
        }
    }
}
Ejemplo alinear texto toggle button Compose

Las opciones ahora entran como una lista de identificadores de drawables para especificar el icono a dibujar. Además el OutlinedButton reduce su contentPadding a 12dp y se establece el tamaño mínimo a 48dp.

Ahora bien, para crear la pantalla que alinea el texto creamos la siguiente función:

@Composable
fun ButtonToggleGroupIconsExample() {
    val options = listOf(
        R.drawable.ic_format_align_center,
        R.drawable.ic_format_align_left,
        R.drawable.ic_format_align_right
    )
    var selectedOption by remember { mutableStateOf(options[0]) }
    var align by remember { mutableStateOf(TextAlign.Center) }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(LoremIpsum(10).values.joinToString(), textAlign = align)

        Spacer(modifier = Modifier.size(16.dp))

        ButtonToggleGroup(
            options = options,
            selectedOption = selectedOption,
            onOptionSelect = { option ->
                selectedOption = option
                align = when (option) {
                    options[0] -> TextAlign.Center
                    options[1] -> TextAlign.Start
                    else -> TextAlign.End
                }
            }
        )
    }
}

Como ves, determinamos el valor TextAlign según el parámetro option percibido por la lambda en onOptionSelect. Lo que permite actualizar el estado del componente Text en su parámetro textAlign y la variable align.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!