CheckBox En Compose

En este tutorial aprenderás sobre el uso del CheckBox en Compose, a fin de crear un componente que permite seleccionar uno o varios ítems de un conjunto. Estos son representados por una casilla que alterna entre dos estados para reflejar la activación/desactivación de la opción en contexto.

Los temas a tratar son:

Ejemplo De CheckBox En Compose

Puedes encontrar todos los ejemplos de este tutorial en el paquete examples/Checkbox del módulo p7_componentes, que se encuentra en GitHub.

Verás que existen archivos Kotlin que corresponden numéricamente a las secciones que verás aquí. En ellos encontrarás funciones componibles que puedes seleccionar previsualizar en CheckboxesScreen.kt.

@Composable
fun CheckboxesScreen() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        // Reemplaza por el ejemplo que deseas ver
        DisabledCheckboxExample()
    }
}

@Composable
@Preview
fun CheckboxesScreenPreview() {
    MaterialTheme {
        Surface {
            CheckboxesScreen()
        }
    }
}

Y si deseas ejecutarlos directamente en el emulador o tu dispositivo, entonces abre ComponentsActivity y reemplaza la invocación en setContent() por CheckboxesScreen:

class ComponentsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    CheckboxesScreen()
                }
            }
        }
    }
}

1. Crear Un CheckBox

Usa la función componible Checkbox() de Compose para dibujar la caja de confirmación en la UI. La firma de esta es:

@Composable
fun Checkbox(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = CheckboxDefaults.colors()
): @Composable Unit

Los parámetros de esta función cumplen con los siguientes objetivos:

  • checked: Determina si el Checkbox está marcado o no
  • onCheckedChange: controlador ejecutado cuando el usuario cambia el estado
  • modifier: la cadena de modificadores aplicado al Checkbox
  • enabled: Determina si está activado o desactivado
  • colors: Representa los colores de la casilla, la marca y el borde en diferentes estados

Por ejemplo:

El uso más simple de un Checkbox es activar o desactivar una sola opción. Por lo que solo requerirás de invocar su función con checked y onCheckedChange como se muestra en el siguiente ejemplo:

@Composable
fun CheckboxExample() {
    val checked = remember { mutableStateOf(true) }
    Checkbox(
        checked = checked.value,
        onCheckedChange = { checked.value = it }
    )
}
Apariencia de Checkbox en Compose
Checkbox en Compose

De forma similar al TextField, necesitamos recordar el estado de verificación de un Checkbox en cada recomposición. Por esta razón usamos la variable checked con la función remember.


2. Checkbox Con Etiqueta

Para mejorar la intención del Checkbox es necesario usar una etiqueta de contenido que represente al ítem asociado con marcar o desmarcar.

Así que crearemos una función componible que use un layout Row para incluir un elemento Text que sirva como etiqueta para la casilla:

@Composable
fun LabelledCheckbox(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    label: String,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: CheckboxColors = CheckboxDefaults.colors()
) {
    Row(
        modifier = modifier.height(48.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {

        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange,
            enabled = enabled,
            colors = colors
        )
        Spacer(Modifier.width(32.dp))
        Text(label)
    }
}

Junto a esta inclusión de la etiqueta también aplicamos state hoisting para desacoplar el estado y el controlador de eventos. De esta forma podremos reutilizar esta función para crear checkboxes con etiqueta en otros lugares.

Por ejemplo:

@Composable
fun CheckboxLabelExample() {
    val checked = remember { mutableStateOf(true) }
    LabelledCheckbox(
        checked = checked.value,
        onCheckedChange = { checked.value = it },
        label = "Checkbox con etiqueta"
    )
}

3. Lista De CheckBoxes

Otro uso es la selección de una o más opciones de una lista de checkboxes. En este caso debes crear la estructura del layout necesaria según el orden y cantidad de ítems que desees mostrar.

Con esto en mente, la siguiente función componible crea el diseño correspondiente:

data class Option( // 1
    var checked: Boolean,
    var onCheckedChange: (Boolean) -> Unit = {},
    val label: String,
    var enabled: Boolean = true
)

@Composable
fun CheckboxList(options: List<Option>, listTitle: String) {// 2
    Column { // 3
        Text(listTitle, textAlign = TextAlign.Justify) // 4
        Spacer(Modifier.size(16.dp)) // 5
        options.forEach { option -> // 6
            LabelledCheckbox( // 7
                checked = option.checked,
                onCheckedChange = option.onCheckedChange,
                label = option.label,
                enabled = option.enabled
            )
        }
    }
}

En el código anterior:

  1. Envolvemos los parámetros del Checkbox en una clase de datos llamada Option
  2. Creamos una función componible llamada CheckboxList que reciba una lista de opciones y el título asociado
  3. Creamos una columna para desplegar verticalmente los elementos
  4. Mostramos un componente Text con el título
  5. Añadimos espacio entre el título y las casillas
  6. Invocamos la función de extensión forEach() desde la lista options con el fin de iterar sobre cada opción
  7. Utilizamos a LabelledCheckbox() para desplegar las casillas con su etiqueta correspondiente

Observemos un ejemplo de la función anterior:

Desplegar una conjunto de cuatro disciplinas (Programación, Diseño, Audio y Arte), de las cuales el usuario puede manifestar interés en múltiples de ellas:

@Composable
fun CheckboxListExample() {
    val disciplines = listOf("Programación", "Diseño", "Audio", "Arte")

    val options = disciplines.map {
        val checked = remember { mutableStateOf(false) }
        Option(
            checked = checked.value,
            onCheckedChange = { checked.value = it },
            label = it,
        )
    }

    CheckboxList(options = options, listTitle = "Disciplinas")
}
Lista de Checkboxes en Compose
Lista de Checkboxes en Compose

El ejemplo anterior muestra cómo a partir de una lista de strings con las disciplinas se crea un mapeo (map())para las opciones que serán parte de la lista.


4. CheckBox Deshabilitado

Usa el atributo enabled en false para desactivar un CheckBox a fin de que no reciba eventos del usuario y aparezca en gris (o color que determines para este estado).

Checkbox deshabilitado en Compose
Checkbox deshabilitado

Por ejemplo:

Supón que estás creando una app de quizzes y tienes una pregunta donde deseas limitar a la selección de sólo dos ítems en una lista de cuatro:

@Composable
fun DisabledCheckboxExample() {
    val gameReleases = listOf( // 1
        "Hitman 3",
        "Monster Hunter World: Iceborne",
        "Days Gone",
        "Pokémon Rumble Rush"
    )

    val options = gameReleases.map { option -> // 2
        val checked = remember { mutableStateOf(false) }
        Option(
            checked = checked.value,
            onCheckedChange = { checked.value = it },
            label = option
        )
    }

    val numberOfMarks = options.count { it.checked } // 3

    if (numberOfMarks == 2) { // 4
        options
            .filterNot { option -> option.checked }
            .forEach { unchecked -> unchecked.enabled = false }
    }

    CheckboxList( // 5
        options = options,
        listTitle = "En la siguiente lista hay dos videojuegos lanzados en el 2021." +
                " Selecciona tus respuestas:"
    )
}
Al marcar dos opciones de respuesta las demás se desactivan

Los pasos de la solución anterior constan de:

  1. Establecemos las opciones de respuesta en una lista de strings
  2. Mapeamos los strings en objetos Option
  3. Contamos la cantidad de respuestas marcadas en cada recomposición a través de count()
  4. Si hay dos selecciones, entonces filtramos aquellas respuestas sin seleccionar y luego las desactivamos
  5. Terminamos usando nuestra función CheckboxList para crear el enunciado de la pregunta junto a las opciones de respuesta

5. CheckBox Indeterminado (TriStateCheckBox)

Compose te permite crear un CheckBox con un estado adicional llamado estado indeterminado. Este es representado por el componente TriStateCheckbox.

Estado indeterminado en TriStateCheckbox
Estado indeterminado en TriStateCheckbox

La siguiente es la firma de su función componible:

@Composable
fun TriStateCheckbox(
    state: ToggleableState,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: CheckboxColors = CheckboxDefaults.colors()
): @Composable Unit

Como ves, recibe el parámetro state para representar su estado a través de la clase enum ToggleableState, la cual contiene tres valores:

  • On: El componente está encendido
  • Off: El componente está apagado
  • Indeterminate: Estado que representa que los valores On/Off no pueden ser determinados

El atributo onClick recibe una lambda para especificar las acciones cuando se hace clic en el Checkbox, que comúnmente será el cambio de estado.

Por ejemplo:

Crear un TriStateCheckbox cuyo estado alterne de la forma Off->On->Indeterminate al ser cliqueado:

@Composable
fun TriStateCheckboxExample() {
    val state = remember { mutableStateOf(ToggleableState.Off) }
    TriStateCheckbox(
        state = state.value,
        onClick = {
            state.value = when (state.value) {
                ToggleableState.Off -> ToggleableState.On
                ToggleableState.On -> ToggleableState.Indeterminate
                ToggleableState.Indeterminate -> ToggleableState.Off
            }
        }
    )
}
Apariencia de TriStateCheckbox en Compose
Apariencia de TriStateCheckbox en Compose

6. Relación Padre-Hijo Entre Checkboxes

El TriStateCheckbox es de utilidad para crear listas de opciones que contienen subselecciones, es decir, Checkboxes con relaciones padre-hijo que facilita la selección o deselección de todos los ítems.

La mezcla de estados en este tipo de relaciones produce los siguientes estados:

  • Al seleccionar el padre se seleccionan todos los hijos
  • Al deseleccionar al padre se deseleccionan todos los hijos
  • A seleccionar algunos hijos, el padre pasa a un estado indeterminado

Con este en mente creemos la función componible CheckboxListWithParent donde se incluya un TriStateCheckbox para representar al padre de la lista:

@Composable
fun CheckboxListWithParent(// 1
    options: List<Option>,
    parentState: ToggleableState,
    onParentClick: () -> Unit,
    parentLabel: String
) {
    Column { // 2
        LabelledTriStateCheckbox( // 3
            state = parentState,
            onClick = onParentClick,
            label = parentLabel
        )

        options.forEach { option -> // 4
            LabelledCheckbox( // 5
                checked = option.checked,
                onCheckedChange = option.onCheckedChange,
                label = option.label,
                enabled = option.enabled,
                modifier = Modifier.padding(start = 32.dp)
            )
        }
    }
}

En el código anterior:

  1. Tomamos una lista de opciones, el estado actual del padre, el controlador de clic del padre y su etiqueta
  2. Creamos una columna
  3. Mostramos en la parte superior al TriStateCheckbox pasándole los atributos correspondientes
  4. Usamos de nuevo a forEach() para mecanizar el dibujado de los n hijos
  5. Creamos cada casilla con etiqueta con padding en el inicio para conseguir una sangría que marque la diferencia entre padre e hijos

Veamos un ejemplo de la función anterior:

Considera la creación de la sección de un filtro que permite seleccionar marcas de autos:

@Composable
fun CheckboxListWithParentExample() {
    val labels = listOf(// 1
        "Interbrand",
        "Toyota",
        "Mercedes Benz",
        "BMW",
        "Honda",
        "Hyundai",
        "Tesla",
        "Ford"
    )

    val childStates = remember { // 2
        val elements = Array(labels.size) { false }
        mutableStateListOf(*elements)
    }

    val options = labels.mapIndexed { index, label -> // 3
        Option(
            checked = childStates[index],
            onCheckedChange = { childStates[index] = it },
            label = label,
        )
    }

    val parentState = remember(*childStates.toTypedArray()) { // 4
        when {
            childStates.all { it } -> ToggleableState.On
            childStates.none { it } -> ToggleableState.Off
            else -> ToggleableState.Indeterminate
        }
    }

    val onParentClick = { // 5
        val derivedState = parentState != ToggleableState.On
        childStates.fill(derivedState)
    }

    CheckboxListWithParent( // 6
        options = options,
        parentState = parentState,
        onParentClick = onParentClick,
        parentLabel = "Marcas"
    )
}
Dependencia padre-hijo entre Checkboxes
Dependencia padre-hijo entre Checkboxes

En el código anterior el algoritmo cumple con:

  1. Partimos de una lista de strings en memoria con las marcas de autos disponibles para el filtro
  2. Creamos un Array de booleanos inicializados en false con el tamaño de la lista de marcas. Luego creamos una lista de estados con mutableStateListOf() que memorice a cada elemento del array, por lo que usamos el operador Spread (*) para satisfacer los argumentos variables
  3. Creamos una lista de opciones con el mapeo indexado de las etiquetas
  4. Determinamos el estado del padre con remember() junto a los valores de los estados de los hijos. Esto hace que en cada recomposición se recalcule el estado del padre si los estados de los hijos cambiaron. Claramente, si todos los hijos están marcados (all()), el estado del padre será On. Si ninguno está marcado (none()), será Off. Y cualquier otro caso será Indeterminate
  5. Actualizamos los estados de todos los hijos cuando se hace clic en el padre. Por lo que usamos la función fill() para asignar el mismo valor a todos los elementos de childStates cuando se obtiene derivedState
  6. Finalizamos creando la lista de checkboxes con relación padre-hijo

7. Cambiar Colores De CheckBox

El atributo colors recibe una instancia del tipo CheckBoxColors para asignar elementos Color a diferentes partes decorativas del CheckBox. Puedes crear una instancia con la función CheckBoxDefaults.colors() y pasar los siguientes parámetros:

@Composable
fun colors(
    checkedColor: Color = MaterialTheme.colors.secondary,
    uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
    checkmarkColor: Color = MaterialTheme.colors.surface,
    disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
    disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
): @Composable CheckboxColors

Donde:

  • checkedColor: Es el color usado en el borde y el fondo de la casilla cuando está marcado
  • uncheckedColor: Es el color usado en el borde cuando está desmarcado
  • checkmarkColor: Color de la marca de verificación
  • disabledColor: Color del borde y casilla cuando está desactivado
  • disabledIndeterminateColor: Color del borde y casilla cuando el TriStateCheckBox (ver última sección) está en estado indeterminado

Por ejemplo:

Cambiar los colores de un Checkbox con las siguientes requerimientos:

  • Color de selección: Púrpura 100
  • Color de marca: Púrpura 900
  • Color deselección: Púrpura 400
  • Color deshabilitado: Gris claro
  • Color deshabilitado + estado indeterminado: Color de selección con alpha de contraste bajo

Basado en la lista anterior creemos una función componible que muestre 5 checkboxes en una columna junto a la etiqueta que especifique su estado actual:

@Composable
@Preview
fun ColoredCheckboxExample() {

    val checkedColor = Color(0xFFE1BEE7)
    val checkmarkColor = Color(0xFF4A148C)
    val uncheckedColor = Color(0xFF7E57C2)
    val disabledColor = Color.LightGray
    val disabledIndeterminateColor = checkedColor.copy(ContentAlpha.disabled)

    Column {
        LabelledCheckbox(
            label = "Enabled+Selected",
            checked = true,
            onCheckedChange = { },
            colors = CheckboxDefaults.colors(
                checkedColor = checkedColor,
                checkmarkColor = checkmarkColor,
            )
        )

        LabelledCheckbox(
            label = "Enabled+Unselected",
            checked = false,
            onCheckedChange = { },
            colors = CheckboxDefaults.colors(
                uncheckedColor = uncheckedColor,
            )
        )

        LabelledCheckbox(
            label = "Disabled+Unselected",
            checked = false,
            onCheckedChange = { },
            colors = CheckboxDefaults.colors(
                disabledColor = disabledColor,
            ),
            enabled = false
        )

        LabelledCheckbox(
            label = "Disabled+Selected",
            checked = true,
            onCheckedChange = { },
            colors = CheckboxDefaults.colors(
                disabledColor = disabledColor,
            ),
            enabled = false
        )

        LabelledTriStateCheckbox(
            label = "Disabled+Indeterminate",
            state = ToggleableState.Indeterminate,
            onClick = { },
            enabled = false,
            colors = CheckboxDefaults.colors(
                disabledIndeterminateColor = disabledIndeterminateColor
            )
        )
    }
}

El resultado del código anterior es el siguiente:

Diferentes valores del parámetro colors en Checkbox

Ú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!