Diálogos En Compose

En este tutorial aprenderás a usar diálogos en Compose para interrumpir al usuario con una ventana emergente que proyecta información urgente, detalles o confirmación de acciones en tu App Android.

Tipos de diálogos en Material Design
Tipos de diálogos en Material Design

Visualmente, un diálogo está compuesto de un contenedor que actúa como superficie, un título opcional en la parte superior, el texto de apoyo para indicar el objetivo y los respectivos botones de confirmación.

En el Material Design existen varios tipos de diálogos con diferentes propósitos para el usuario. Nuestro objetivo será aprender a implementarlos en Jetpack Compose, procesar los eventos de sus botones de acción y cambiar su estilo.


Ejemplos De Diálogos En Compose

Puedes encontrar el código de este tutorial en el siguiente repositorio GitHub:

El paquete examples/Dialog del módulo p7_componentes contiene un archivo por cada cada apartado que se encuentra en este post:

Paquete para ejemplos de diálogos en Compose

En DialogScreen.kt encontrarás la pantalla principal donde se ejecutan los ejemplos.

Ejemplos de diálogos en Compose
Ejemplos de diálogos en Compose

Esta consiste de una función componible cuyo propósito es crear una lista prediseñada en la parte superior. Esta consta de cinco ítems con los nombres de las secciones del tutorial. Y en la parte inferior hay un texto que cambia su contenido según la acción elegida en los diálogos de ejemplo.

Con esto claro, comencemos con el primer apartado, los diálogos de alerta.


1. Diálogo De Alerta

Anatomía de un diálogo de alerta
Anatomía de un diálogo de alerta

Un diálogo de alerta o Alert Dialog se despliega para interrumpir a los usuarios con información, detalles o acciones que son urgentes en el contexto actual de la App.

Usa una de las dos versiones de la función componible AlertDialog() para desplegar un diálogo de alerta:

// Versión con botones por defecto
@Composable
fun AlertDialog(
    onDismissRequest: (() -> Unit)?,
    confirmButton: (@Composable () -> Unit)?,
    modifier: Modifier? = Modifier,
    dismissButton: (@Composable () -> Unit)? = null,
    title: (@Composable () -> Unit)? = null,
    text: (@Composable () -> Unit)? = null,
    shape: Shape? = MaterialTheme.shapes.medium,
    backgroundColor: Color? = MaterialTheme.colors.surface,
    contentColor: Color? = contentColorFor(backgroundColor),
    properties: DialogProperties? = DialogProperties()
): Unit

// Versión con botones personalizables
@Composable
fun AlertDialog(
    onDismissRequest: (() -> Unit)?,
    buttons: (@Composable () -> Unit)?,
    modifier: Modifier? = Modifier,
    title: (@Composable () -> Unit)? = null,
    text: (@Composable () -> Unit)? = null,
    shape: Shape? = MaterialTheme.shapes.medium,
    backgroundColor: Color? = MaterialTheme.colors.surface,
    contentColor: Color? = contentColorFor(backgroundColor),
    properties: DialogProperties? = DialogProperties()
): Unit

De las firmas anteriores, cada parámetro determina:

  • onDismissRequest: Función que se ejecutan cuando el usuario intenta cerrar el diálogo haciendo clic por fuera del mismo o presionando el back button
  • confirmButton: Botón de confirmación para la acción propuesta por el diálogo
  • modifier: Modificador para la caja del diálogo que es dibujada
  • dismissButton: Botón para descartar al diálogo
  • title: Texto para el título del diálogo
  • text: Texto de apoyo
  • shape: Forma de los bordes del diálogo
  • backgroundColor: Color de fondo
  • contentColor: Color aplicado sobre los hijos del diálogo en su sección de contenido
  • properties: Propiedades del diálogo asociadas a la plataforma
  • buttons: Función componible para crear el layout personalizado de los botones

Veamos un ejemplo de la creación de un diálogo de alerta.

Ejemplo: Diálogo De Eliminación

Supón que el usuario intenta eliminar un registro en la App y tu deseas pedirle una confirmación de esta acción para que no pierda sus datos por equivocación. La siguiente ilustración muestra el diálogo de alerta que cumple este cometido:

Diálogo de alerta para eliminar ítem
Diálogo de alerta para eliminar ítem

Para producir este resultado, crearemos el archivo 01_AlertDialog.kt y a su vez implementaremos una función componible llamada TutorialAlertDialog(). En su interior, usaremos el componente AlertDialog de la siguiente forma:

@Composable
fun TutorialAlertDialog( // (1)
    titleText: String? = null,
    bodyText: String,
    confirmButtonText: String,
    onConfirm: () -> Unit,
    cancelButtonText: String,
    onCancel: () -> Unit,
    onDismiss: () -> Unit
) {
    // (2)
    val title: @Composable (() -> Unit)? = if (titleText.isNullOrBlank()) 
        null
    else {
        { Text(text = titleText) }
    }

    // (3)
    AlertDialog(
        title = title,
        text = {
            Text(
                text = bodyText
            )
        },
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(onClick = { // (4)
                onConfirm()
                onDismiss()
            }) {
                Text(text = confirmButtonText)
            }
        },
        dismissButton = {
            TextButton(onClick = { // (5)
                onCancel()
                onDismiss()
            }) {
                Text(text = cancelButtonText)
            }
        }
    )
}

Del código anterior:

  1. Incluimos como parámetros los textos para título, cuerpo y botones. Además de las funciones a ejecutarse al confirmar, cancelar y cerrar
  2. Ya que titleText es opcional, creamos una variable title que tome el valor de null, en caso de que titleText esté vacío, o la invocación de la función Text() para crear el título
  3. Invocamos a la función componible AlertDialog() y pasamos los parámetros de TutorialAlertDialog()
  4. Pudimos haber asignado directamente onConfirm al parámetro onClick del TextButton, pero como deseamos que el diálogo se cierre, invocamos también a onDismiss en una lambda multilínea
  5. Al igual que con confirmButton, dismissButton invoca a onDismiss luego de la acción del botón

Con el componente definido, ahora podemos invocarlo y controlar su estado de visibilidad. Para ello creamos la siguiente función componible Example1() en DialogScreen.kt:

@Composable
fun Example1( // (1)
    showDialog: (Boolean) -> Unit,
    setActionText: (String) -> Unit
) {
    TutorialAlertDialog(
        bodyText = "¿Eliminar ítem?",
        confirmButtonText = "ELIMINAR",
        onConfirm = {
            setActionText("Ejemplo 1 -> 'ACEPTAR'") // (2)
        },
        cancelButtonText = "CANCELAR",
        onCancel = {
            setActionText("Ejemplo 1 -> 'CANCELAR'")
        },
        onDismiss = { showDialog(false) } // (3)
    )
}

Example1 tiene como propósito:

  1. Recibir la función de asignación del estado de visibilidad del diálogo y el estado del texto de DialogScreen(). Ambos serán usados para varios varios parámetros de TutorialAlertDialog()
  2. Cuando se presiona el botón de confirmación, usamos la forma de invocación de setActionText para modificar el texto por un mensaje sobre el botón
  3. El parámetro onDismiss toma una lambda donde asignamos false a showDialog con el fin de cerrar al diálogo de alerta

Ahora, ¿donde invocamos a Example1?

La respuesta es: en la función componible que crea las filas de la lista. Es decir, la función SectionRow():

@Composable
fun SectionRow(
    section: String,
    index: Int,
    setActionText: (String) -> Unit
) {
    // (1)
    val (dialogOpen, showDialog) = remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .height(48.dp)
            .fillMaxWidth()
            .clickable(
                onClick = {
                    showDialog(true) // (2)
                })
            .padding(horizontal = 16.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            modifier = Modifier.fillMaxWidth(),
            text = section
        )
    }

    if (!dialogOpen) return // (3)

    when (index) {
        0 -> Example1(showDialog, setActionText) // (4)
        1 -> Example2(dialogOpen, showDialog, setActionText)
        2 -> Example3(dialogOpen, showDialog, setActionText)
        3 -> Example4(dialogOpen, showDialog, setActionText)
        4 -> Example5(dialogOpen, showDialog, setActionText)
    }
}

Esta se encarga de crear cada ítem y de manejar la creación de los diálogos:

  1. Se declara el estado para el diálogo asociado a la fila, usando desestructuración para obtener a dialogOpen y showDialog. Claramente el valor inicial de la visibilidad es false
  2. Debido a que cada fila tiene un modificador clickable(), invocamos al segundo componente showDialog() con true para hacer visible al diálogo
  3. Verificamos en cada recomposición si el diálogo es visible, en caso negativo, finalizamos la función con return
  4. Si es visible, entonces invocamos la función asociada al índice de la lista. En este caso particular, si es 0 se llama a Example1()

Por último, modifica ComponentsActivity para que ejecute a DialogScreen:

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

Ejecuta el módulo p7_componentes, haz clic en el primer ítem y verás al diálogo de alerta:

El AlertDialog se despliega al presionar el primer ítem de la lista

Apilar Acciones

Diálogo de alerta con botones apilados
Diálogo de alerta con botones apilados

Si las etiquetas de las acciones son muy largas para acomodarlas horizontalmente en el contenedor, la función AlertDialog automáticamente las apilará para mejorar la toma de decisión en un espacio más acorde.


2. Diálogo Simple

Anatomía de un diálogo simple
Anatomía de un diálogo simple

Este tipo de diálogo le muestra una lista de elementos al usuario con el fin de aplicar una acción inmediata al seleccionar una opción. Por esta razón no tienen botones de texto, ya que cada elemento de la lista actúa como posible acción.

La única forma de cerrar el diálogo sin realizar ninguna acción es presionando por fuera del mismo.

Veamos cómo implementarlo.

Ejemplo: Diálogo Para Seleccionar Cuentas

Ejemplo de diálogo de selección de cuentas en Compose
Ejemplo de diálogo de selección de cuentas en Compose
(Avatares obtenidos desde uifaces)

Tomemos como ilustración un caso de uso donde deseas cargar los datos de una cuenta de usuario en tu App Android. Debido a que esta permite múltiples usuarios, las cuentas se mostrarán como una lista junto a un ítem para añadirlas como se muestra en la anterior imagen.

¿Cómo implementar este diálogo?

Si revisas la implementación de la función AlertDialog(), verás que para crear su contenido usa la función Dialog(). Por lo que podemos hacer exactamente lo mismo para crear nuestro diálogo con lista.

No obstante, usaremos a la segunda variación de AlertDialog(), que recibe una función componible en su parámetro buttons, la cual toma todo el espacio del contenedor si no existe texto de apoyo.

Estableciendo lo anterior, ¿Qué debemos hacer?

  1. Crear una clase de datos que represente las cuentas en la interfaz
  2. Crear la función componible que represente al diálogo de cuentas
  3. Crear la función componible que dibuje al contenido del parámetro buttons
  4. Crear la función componible para los ítems de cuentas
  5. Crear la función componible para el ítem de añadir cuenta

Si abres el archivo 02_SimpleDialog.kt, podrás encontrar las tareas materializadas así:

1. Crear Clase Para Cuentas

La clase Account representa a los ítems en la lista del diálogo:

data class Account(
    val email: String,
    @DrawableRes val avatar: Int = R.drawable.no_avatar
)

2. Función AccountsDialog()

Recibe como parámetros una lista de objetos Account y tres funciones que se ejecutarán al cerrar el diálogo, seleccionar un ítem y añadir una cuenta.

@Composable
fun AccountsDialog(
    accounts: List<Account>,
    onDismiss: () -> Unit,
    onAccountClick: (Account) -> Unit,
    onAddAccountClick: () -> Unit
) {

    AlertDialog(
        onDismissRequest = onDismiss,
        title = {
            Text(
                text = "Seleccionar cuenta",
                style = MaterialTheme.typography.h6
            )
        },
        buttons = {
            AccountsDialogContent(accounts, onAccountClick, onAddAccountClick)
        }
    )
}

Como ves, invoca a AlertDialog y asigna a buttons a la función AccountsDialogContent() para crear el contenido de contenedor del diálogo.

3. Función AccountsDialogContent()

Con esta función añadimos los ítems de las cuentas y la opción final para añadirlas.

Composición del contenido del diálogo
Composición del contenido del diálogo

En código tendremos:

@Composable
private fun AccountsDialogContent(
    accounts: List<Account>,
    onAccountClick: (Account) -> Unit,
    onAddAccountClick: () -> Unit
) {

    Column {
        Spacer(Modifier.height(20.dp))

        accounts.forEach { account ->
            AccountRow(account, onAccountClick)
        }

        AddAccountRow(onAddAccountClick)

        Spacer(Modifier.height(8.dp))
    }
}

Las filas para las cuentas se crean usando la función forEach() sobre las cuentas. En su interior se invoca a la función AccountRow().

Nota: Si el diálogo albergará una gran cantidad de opciones o desconoces el número, lo mejor es usar LazyColumn (todo).

Y la acción para añadir se proyecta al final con AddAccountRow().

4. Función AccountRow()

Las cuentas se tratan de los elementos Image() y Text(). La imagen usa en su parámetro painter el valor del atributo avatar de la cuenta. Y el texto usa el atributo email:

@Composable
fun AccountRow(account: Account, onAccountClick: (Account) -> Unit) {
    Row(
        Modifier
            .clickable(onClick = { onAccountClick(account) })
            .fillMaxWidth()
            .height(56.dp)
            .padding(horizontal = 24.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Image(
            modifier = Modifier
                .clip(CircleShape)
                .size(40.dp),
            painter = painterResource(id = account.avatar),
            contentDescription = "Cuenta de ${account.email}"
        )

        Spacer(Modifier.width(20.dp))

        Text(text = account.email)
    }
}

Ambos elementos se encuentran dentro de un layout Row() con el centrado vertical correspondiente. Adicional pasamos a onAccountClick como argumento de clickable().

5. Función AddAccountRow()

La fila para añadir la cuenta es similar a la de la selección de cuenta, solo que en lugar del avatar se usa un elemento Box() que recubre un Icon() y en vez del correo va el texto de la acción:

@Composable
fun AddAccountRow(onAddAccountClick: () -> Unit) {
    Row(
        modifier = Modifier
            .clickable(onClick = onAddAccountClick)
            .fillMaxWidth()
            .height(56.dp)
            .padding(horizontal = 24.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .background(color = Color.LightGray),
            contentAlignment = Alignment.Center
        ) {
            Icon(
                modifier = Modifier
                    .clip(CircleShape)
                    .size(24.dp),
                imageVector = Icons.Filled.Add,
                contentDescription = "Opción para añadir cuenta"
            )
        }

        Spacer(Modifier.width(20.dp))

        Text(text = "Añadir cuenta")
    }
}

Ahora, si abres DialogScreen.kt, verás que el diálogo de cuentas es invocado desde Example2():

@Composable
fun Example2(
    showDialog: (Boolean) -> Unit,
    setActionText: (String) -> Unit
) {

    val accounts = listOf(
        Account("drjlaw@outlook.com", R.drawable.ac1),
        Account("sopwith@sbcglobal.net", R.drawable.ac2),
        Account("rmcfarla@att.net", R.drawable.ac3)
    )

    AccountsDialog(
        accounts = accounts,
        onAccountClick = { account ->
            setActionText("Ejemplo 2 -> '${account.email}'")
            showDialog(false)
        },
        onAddAccountClick = {
            setActionText("Ejemplo 2 -> 'Añadir cuenta'")
            showDialog(false)
        },
        onDismiss = { showDialog(false) }
    )
}

Se crea una lista de tres cuentas para pasar al parámetro accounts. Cuando se selecciona una cuenta o intentas añadir una, cambiamos el texto de la pantalla principal con setActionText.

Al igual que en el primer ejemplo, usamos showDialog para cerrar el diálogo una vez ocurran los eventos.

Si corres la App y seleccionas el ítem 2, podrás ver el diálogo simple:


3. Diálogo De Confirmación Con Selección Única

Anatomía de un diálogo de confirmación
Anatomía de un diálogo de confirmación

Los diálogos de confirmación con selección única, le permiten al usuario elegir una sola opción entre varias. Al igual que el diálogo de alerta, trae consigo dos botones de acción para confirmar o cancelar la selección.

Como notas, es un diálogo con RadioButtons en su contenedor, por lo que es necesario crear una implementación para este diseño.

Veamos.

Ejemplo: Diálogo Para Elegir Formato De Fechas

Ejemplo de diálogo de confirmación de formatos de fecha
Ejemplo de diálogo de confirmación de formatos de fecha

En este ejemplo asumimos que se requiere mostrar un diálogo con una lista de formatos de fecha, para que el usuario decida cuál prefiere ver en la presentación de datos.

De forma similar al ejemplo de diálogo simple, las tareas se resumen a:

  1. Crear una función general para el diálogo de confirmación
  2. Crear una función que dibuje el contenido
  3. Crear una función para cada fila de RadioButtons

Hagamos un recorrido en el archivo 03_ConfirmationDialog.kt del repositorio para ver el código de cada función.

1. Función ConfirmationDialog()

Esta función es la encargada de establecer como parámetros los atributos comunes como del diálogo, con el objetivo de pasarlos a llamada de AlertDialog.

@Composable
fun ConfirmationDialog(
    items: List<String>, // (1)
    titleText: String,
    confirmButtonText: String,
    onConfirm: (String) -> Unit,
    cancelButtonText: String,
    onCancel: () -> Unit,
    onDismiss: () -> Unit
) {
    val (selectedOption, selectOption) = remember { mutableStateOf(items.first()) } // (2)

    AlertDialog(
        onDismissRequest = onDismiss,
        title = {
            Text(
                text = titleText,
                style = MaterialTheme.typography.h6
            )
        },
        buttons = {
            ConfirmationDialogContent(
                items = items,
                selectedOption = selectedOption,
                selectOption = selectOption,
                confirmButtonText = confirmButtonText,
                onConfirm = {
                    onConfirm(selectedOption) // (3)
                },
                cancelButtonText = cancelButtonText,
                onCancel = onCancel
            )
        }
    )
}

Aspectos a destacar:

  1. La lista de opciones es de tipo String, ya que no aplicaremos lógica adicional cuando se cambie entre selecciones
  2. Además es necesario declarar un estado para sostener la opción seleccionada en las recomposiciones. Por esta razón tenemos a los componentes selectedOption y selectOption
  3. El parámetro onConfirm es un tipo función (String)->Unit porque deseamos comunicar hacia el exterior el valor seleccionado

2. Función ConfirmationDialogContent()

Aquí es donde diseñamos el cuerpo del diálogo, el cual consiste de un elemento Column con divisores, las opciones y la sección de botones.

@Composable
fun ConfirmationDialogContent(
    items: List<String>,
    selectedOption: String,
    selectOption: (String) -> Unit,
    confirmButtonText: String,
    onConfirm: () -> Unit,
    cancelButtonText: String,
    onCancel: () -> Unit
) {

    Column(Modifier.selectableGroup()) { // (1)
        Spacer(modifier = Modifier.height(20.dp))

        Divider()

        items.forEach { item ->
            ItemRow(
                item = item,
                selected = item == selectedOption, // (2)
                select = selectOption
            )
        }

        Divider()

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(52.dp)
                .padding(8.dp),
            horizontalArrangement = Arrangement.End
        ) {
            TextButton(onClick = onCancel) {
                Text(text = cancelButtonText) // (3)
            }
            Spacer(modifier = Modifier.width(8.dp))
            TextButton(onClick = onConfirm) {
                Text(text = confirmButtonText) // (3)
            }
        }
    }
}

Detalles de importancia del código anterior:

  1. Usa el modificador selectableGroup() para presentar a la columna como un grupo de elementos seleccionables ante el sistema de accesibilidad
  2. La función creadora de opciones, ItemRow(), recibe un booleano que determina si está marcado su RadioButton. Para inferirlo, comparamos el valor de la iteración con el estado actual en selectedOption
  3. Los botones reciben directamente los textos y las funciones de a ejecutarse por el parámetro onClick

3. Función ItemRow()

La función ItemRow() construye la fila con un RadioButton y un texto adyacente.

@Composable
fun ItemRow(
    item: String,
    selected: Boolean,
    select: (String) -> Unit
) {
    Row(
        modifier = Modifier
            .selectable(
                selected = selected,
                onClick = { select(item) },
                role = Role.RadioButton
            )
            .fillMaxWidth()
            .padding(start = 24.dp, end = 24.dp)
            .height(48.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        RadioButton(selected = selected, onClick = null)

        Spacer(modifier = Modifier.width(32.dp))

        Text(text = item)
    }
}

Como deseamos que toda la fila actué como parte de la selección del radio, le aplicamos el modificador selectable().

En el momento en que se toque la fila, invocamos a select para modificar el estado en ConfirmationDialog.

Ahora bien, debido a que el RadioButton no será el encargado de ejecutar las acciones de selección, pasamos null a su parámetro onClick.

Mostrar Diálogo De Confirmación

La función Example3() de DialogScreen.kt esclarece cómo invocar a la función ConfirmationDialog.

@Composable
fun Example3(
    showDialog: (Boolean) -> Unit,
    setActionText: (String) -> Unit
) {
    val formats = listOf(
        "MM/DD/AA",
        "DD/MM/AA",
        "AA/MM/DD",
        "Mes D, AA"
    )

    ConfirmationDialog(
        items = formats,
        titleText = "Formato De Fecha",
        confirmButtonText = "ACEPTAR",
        onConfirm = { format ->
            setActionText("Ejemplo 3 -> '$format'")
            showDialog(false)
        },
        cancelButtonText = "CANCELAR",
        onCancel = {
            setActionText("Ejemplo 3 -> 'CANCELAR'")
            showDialog(false)
        },
        onDismiss = {
            showDialog(false)
        }
    )
}

El ejemplo anterior crea la lista de cuatro formatos de fecha y la pasa a ConfirmationDialog. Tanto la función de confirmar como la de cancelar, ejecutan un cambio al texto en DialogScreen.

Al correr y ejecutar la App el resultado será:

Ejemplo de diálogo selector de formatos de fecha
Ejemplo de diálogo selector de formatos de fecha

4. Diálogo De Confirmación Con Selección Múltiple

Anatomía de diálogo de confirmación con selección múltiple
Anatomía de diálogo de confirmación con selección múltiple

En el caso en que desees permitir la selección múltiple, entonces crea el diálogo con CheckBoxes en su contenedor.

Su diseño es exactamente igual al diálogo de selección única, divisores entre el título, una lista de filas y la sección de botones. La única diferencia la notarás en la conservación de múltiples opciones en el estado del componible.

Ejemplo: Diálogo Para Etiquetar

Ejemplo de diálogo de confirmación con selección múltiple
Ejemplo de diálogo de confirmación con selección múltiple

Supongamos que se necesita permitir al editor de un blog asignar etiquetas a los artículos que escribe. Los posibles valores son: Datos, Interfaz Gráfica, Conectividad y Segundo Plano.

Para implementarlo seguimos los mismos pasos del ejemplo anterior. Esto puedes confirmarlo abriendo el archivo 04_MultiChoiceConfirmationDialog.kt.

1. Función MultiChoiceConfirmationDialog()

Representa al componente del diálogo de confirmación con múltiple selección:

@Composable
fun MultiChoiceConfirmationDialog(
    items: List<String>,
    titleText: String,
    confirmButtonText: String,
    onConfirm: (List<String>) -> Unit, // (1)
    cancelButtonText: String,
    onCancel: () -> Unit,
    onDismiss: () -> Unit
) {
    val (selectedOptions, selectOptions) = remember {
        mutableStateOf(emptyList<String>()) // (2)
    }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = {
            Text(
                text = titleText,
                style = MaterialTheme.typography.h6
            )
        },
        buttons = {
            DialogContent(
                items = items,
                checkedOptions = selectedOptions,
                selectOptions = selectOptions,
                onConfirm = { onConfirm(selectedOptions) },// (3)
                onCancel = onCancel,
                onDismiss = onDismiss,
                confirmButtonText = confirmButtonText,
                cancelButtonText = cancelButtonText
            )
        }
    )
}

Las instrucciones de esta función son casi iguales a ConfirmationDialog, pero se diferencian en:

  1. El parámetro onConfirm es tipo (List<String)->Unit en vista de que notificaremos una lista de varios elementos marcados
  2. En consecuencia del punto anterior, el estado a conservar será una lista de Strings, donde el valor inicial es una lista vacía
  3. Pasamos una lambda a DialogContent con la invocación de onConfirm sobre las opciones seleccionadas

2. Función DialogContent()

La función para crear el contenido repite la estructura vista en el ejemplo 3, no obstante ciertos elementos cambian:

@Composable
private fun DialogContent(
    items: List<String>,
    checkedOptions: List<String>,
    selectOptions: (List<String>) -> Unit,
    confirmButtonText: String,
    onConfirm: () -> Unit,
    cancelButtonText: String,
    onCancel: () -> Unit,
    onDismiss: () -> Unit
) {

    Column {
        Spacer(modifier = Modifier.height(20.dp))
        Divider()

        items.forEach { currentItem ->
            val isChecked = currentItem in checkedOptions // (1)
            ItemRow(
                item = currentItem,
                checked = isChecked,
                onValueChange = { checked ->
                    val checkedItems = checkedOptions.toMutableList()

                    if (checked)
                        checkedItems.add(currentItem) // (2)
                    else
                        checkedItems.remove(currentItem) // (3)

                    selectOptions(checkedItems) // (4)
                })
        }

        Divider()

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(52.dp)
                .padding(8.dp),
            horizontalArrangement = Arrangement.End
        ) {
            TextButton(onClick = {
                onCancel()
                onDismiss() // (5)
            }) {
                Text(text = cancelButtonText)
            }
            Spacer(modifier = Modifier.width(8.dp))
            TextButton(onClick = {
                onConfirm()
                onDismiss()
            }) {
                Text(text = confirmButtonText)
            }
        }
    }
}

En virtud de que manejamos múltiples opciones, debemos:

  1. Verificar si el ítem actual está marcado en cada recomposición con el operador in
  2. Añadir el ítem a la lista de selecciones si está marcado
  3. Remover el ítem en caso contrario
  4. Actualizar el estado de elementos seleccionados con el parámetro selectOptions
  5. Invocar al parámetro onDismiss al final de la lambda asignada al parámetro onClick de los botones. Este enfoque es válido si sabes que la única acción de onDismiss es cerrar el diálogo

3. Función ItemRow()

En contraste con la función que produce filas con RadioButtons, ItemRow presenta un CheckBox junto a un texto:

@Composable
private fun ItemRow(
    item: String,
    checked: Boolean,
    onValueChange: (Boolean) -> Unit
) {
    Row(
        modifier = Modifier
            .toggleable(
                value = checked,
                onValueChange = onValueChange,
                role = Role.Checkbox
            )
            .fillMaxWidth()
            .padding(start = 24.dp, end = 24.dp)
            .height(48.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(checked = checked, onCheckedChange = null)

        Spacer(modifier = Modifier.width(32.dp))

        Text(text = item)
    }
}

Pero esta vez cambiamos al modificador selectable() por toggleable() con el objeto de otorgar la capacidad de cambiar entre marcado y desmarcado.

Mostrar Diálogo De Etiquetas

La función Example4() de DialogScreen.kt materializa la solución final para mostrar las cuatro etiquetas con las que puede ser marcado un post:

@Composable
fun Example4(
    showDialog: (Boolean) -> Unit,
    setActionText: (String) -> Unit
) {
    val tags = listOf(
        "Datos",
        "Interfaz Gráfica",
        "Conectividad",
        "Segundo Plano"
    )
    
    MultiChoiceConfirmationDialog(
        items = tags,
        titleText = "Etiquetar como:",
        confirmButtonText = "ACEPTAR",
        onConfirm = { selectedTags: List<String> ->
            setActionText(
                if (selectedTags.isNotEmpty())
                    "Ejemplo 4 -> ${selectedTags.joinToString()}"
                else
                    "Ejemplo 4 -> 'Sin etiquetas'"
            )
        },
        cancelButtonText = "CANCELAR",
        onCancel = {
            setActionText("Ejemplo 4 -> 'CANCELAR'")
        },
        onDismiss = {
            showDialog(false)
        }
    )
}

Si prestas atención a la lambda de onConfirm, el parámetro selectedTags representa a la lista de etiquetas seleccionadas cuando se presionó al botón ACEPTAR. Su valor permite imprimir la lista de etiquetas separadas por comas si no está vacío, o el aviso en caso contrario.

Ejecutando el aplicativo con y presionando el ítem cuatro de la lista, el diálogo de las etiquetas aparecerá:

Diálogo para etiquetar post
Diálogo para etiquetar post

5. Cambiar Estilo De Diálogos

Ejemplo de cambio de estilo en un AlertDialog
Ejemplo de cambio de estilo en un AlertDialog

Para terminar estudiemos el cambio de los aspectos visuales a partir de los parámetros de AlertDialog.

Tomemos como caso el diálogo que aparece en la imagen previa. Se trata de un diálogo de alerta usado como aviso informativo, para que el usuario se de cuenta de que quedan cinco unidades de un producto.

El código para crearlo lo encuentras en el archivo 05_StylingDialogs.kt.

@Composable
fun StyledAlertDialog(
    bodyText: String,
    buttonText: String,
    onConfirm: () -> Unit,
    onDismiss: () -> Unit
) {
    AlertDialog(
        onDismissRequest = onDismiss,
        text = {
            Text(bodyText)
        },
        confirmButton = {
            TextButton(
                onClick = {
                    onConfirm()
                    onDismiss()
                },
                colors = ButtonDefaults.textButtonColors(contentColor = Color.White)
            ) {
                Text(text = buttonText)
            }
        },
        shape = RoundedCornerShape( 
            topEndPercent = 50,
            bottomStartPercent = 50
        ),
        backgroundColor = Color(0xFF311b92), 
        contentColor = Color.White 
    )
}

Con el parámetro shape pasamos una instancia de RoundedCornerShape para redondear los bordes superior final e inferior inicial en 50% de la forma original.

El color de la superficie (backgroundColor) sobre la que se proyecta el diálogo lo establecemos como un tono Deep Purple 900.

Y el color aplicado al texto de soporte (contentColor) lo establecemos en blanco con Color.White.

La invocación puedes verla desde Example5():

@Composable
fun Example5(
    showDialog: (Boolean) -> Unit,
    setActionText: (String) -> Unit
) {

    StyledAlertDialog(
        bodyText = "Quedan solo cinco unidades de este producto.",
        buttonText = "Aceptar",
        onConfirm = {
            setActionText("Ejemplo 5 -> 'ACEPTAR'")
        },
        onDismiss = {
            showDialog(false)
        },
    )
}

Al ejecutar y seleccionar el quinto ítem de la lista, verás el diálogo personalizado así:

Nota: Asimismo es posible conseguir el mismo resultado, cambiando el tema del diálogo al crear una instancia de MaterialTheme donde especifiques los colores, formas y tipografía de los elementos en sus diferentes tamaños.

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