Índice de contenido
- Diálogos con Jetpack Compose (el enfoque moderno)
- Manejo del estado del diálogo
- Mostrando el diálogo
- ¿Qué hacen remember y mutableStateOf exactamente?
- Preview vs. Emulador
- Diálogo de confirmación simple
- Diálogo con una lista de opciones
- Diálogo completamente personalizado
- Ejemplo con lista interactiva
- Diálogos con XML y Kotlin (enfoque legacy)
- Creando un diálogo básico
- Diálogo simple de mensaje
- Diálogo de confirmación
- Diálogo de selección simple (lista)
- Diálogo de selección simple con radio buttons
- Diálogo de selección múltiple (checkboxes)
- Diálogo personalizado con layout XML
Los diálogos (dialogs) en Android son pequeñas ventanas modales que aparecen sobre el contenido principal para solicitar al usuario que tome una decisión o ingrese información adicional. Son un componente fundamental para la interacción con el usuario y se pueden personalizar por completo: desde su apariencia visual hasta los botones de acción que exponen.
Siguiendo con el recorrido por los componentes de Android, hoy le toca el turno a los diálogos.
Estas son esas pequeñas ventanitas que podemos utilizar para múltiples situaciones: mostrar información al usuario, confirmar o cancelar una acción crítica, presentar listas de opciones, o incluso renderizar formularios completos. Es un elemento bastante abierto y flexible que, bien utilizado, mejora mucho la experiencia de usuario.
Actualmente existen dos enfoques principales para crear diálogos en Android: el moderno y recomendado Jetpack Compose, y el sistema de vistas tradicional basado en XML usando la biblioteca de Material Components.
En esta entrada exploraremos ambos métodos, comenzando por el enfoque moderno con Jetpack Compose. Si vienes del capítulo anterior, donde aprendimos a implementar el FloatingActionButton en Android Studio y sus variantes, este siguiente paso te resultará muy natural.
Diálogos con Jetpack Compose (el enfoque moderno)
Jetpack Compose es el toolkit de UI declarativo y moderno de Google. Crear diálogos con Compose es más simple e intuitivo que con el sistema de vistas tradicional, ya que todo el estado y la lógica viven en el mismo lugar. El componente principal para esto es el composable AlertDialog.
Manejo del estado del diálogo
Los diálogos son componentes un poco más complejos que un botón. A diferencia de los botones, que simplemente colocamos en la pantalla, los diálogos necesitan activarse y desactivarse, lo que implica manejar un estado.
Para eso creamos una variable de estado llamada showDialog. Es una variable (var) y no una constante (val) porque su valor va a cambiar a lo largo del ciclo de vida del componente:
var showDialog by remember { mutableStateOf(false) }Aquí aparecen dos conceptos nuevos: remember y mutableStateOf.
Si vienes de Flutter, Vue, React o cualquier otro framework reactivo, esto te resultará familiar. Es básicamente la respuesta de Compose al manejo de estado reactivo.
Si declaráramos una variable normal —como var showDialog = false— y luego la cambiásemos al hacer clic en un botón, la interfaz no se actualizaría automáticamente. Para que ese redibujado ocurra de forma automática, necesitamos envolver el valor en un estado reactivo.
Con mutableStateOf le decimos a Compose: "oye, esta variable es reactiva". Cuando su valor cambie, Compose va a recomponer el componente que la esté leyendo y actualizará la interfaz de forma automática.
Por su parte, remember garantiza que ese estado se conserve entre recomposiciones. Sin remember, el estado se reiniciaría cada vez que Compose redibujara el componente, perdiéndose el valor anterior.
Mostrando el diálogo
La lógica de visibilidad es sencilla: usamos un condicional estándar de Kotlin. Si showDialog es true, mostramos el diálogo; si es false, no se renderiza nada.
El parámetro onDismissRequest es el callback que se ejecuta cuando el usuario toca fuera del diálogo o presiona el botón de retroceso. Lo más habitual es cerrarlo poniendo showDialog de vuelta en false.
Dentro del AlertDialog definimos sus partes principales:
title: el título del diálogo.text: el cuerpo del mensaje.confirmButton: el botón de confirmación.dismissButton: el botón de cancelación.
En ambos botones cerramos el diálogo al hacer clic, aunque la lógica real de negocio dependerá de lo que necesites hacer: borrar un elemento, confirmar una operación crítica, etc.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
class MainActivity : ComponentActivity() {
@Composable
fun EjemploDialogoCompose() {
// 1. Definimos el estado (¿se muestra el diálogo?)
var showDialog by remember { mutableStateOf(false) }
// Botón que activa el diálogo
Button(onClick = { showDialog = true }) {
Text("Eliminar elemento")
}
// 2. Declaramos el Diálogo
if (showDialog) {
AlertDialog(
onDismissRequest = {
// Se ejecuta cuando el usuario toca fuera del diálogo o pulsa "Atrás"
showDialog = false
},
title = {
Text(text = "Confirmar eliminación")
},
text = {
Text(text = "¿Estás seguro de que deseas borrar este archivo permanentemente?")
},
confirmButton = {
TextButton(onClick = {
showDialog = false
// Aquí iría tu lógica de borrado
}) {
Text("Confirmar")
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Cancelar")
}
}
)
}
}
}
@Preview(showBackground = true)
@Composable
fun Preview() {
MyProyectAndroidTheme {
EjemploDialogoCompose()
}
}¿Qué hacen remember y mutableStateOf exactamente?
En resumen:
rememberpermite que el estado se conserve entre recomposiciones. Sin él, el valor se reiniciaría cada vez que Compose redibujara el componente.mutableStateOfcrea un estado observable: cuando su valor cambia, Compose sabe que debe recomponer los composables que lo lean.
Cuando cambiamos el valor de showDialog, Compose detecta el cambio, recompone el componente y actualiza la interfaz.
Si el valor pasa a true, el diálogo aparece. Si vuelve a false, desaparece.
Compose puede redibujar la pantalla muchas veces, y eso es totalmente normal. Lo importante no es el valor en sí, sino que esté envuelto en un estado reactivo para que Compose pueda rastrear sus cambios.
Preview vs. Emulador
El @Preview de Android Studio solo sirve para ver una maqueta estática de la interfaz. No tiene funcionalidad real ni ejecuta lógica de negocio.
Para ver el comportamiento real del diálogo —que aparezca al pulsar el botón y desaparezca al confirmar o cancelar—, necesitas ejecutar la app en un emulador o dispositivo físico.
Dale al botón de Play, selecciona el emulador y espera a que arranque. En la MainActivity, elimina o comenta lo que no necesites y llama al composable del diálogo que creaste:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyProyectAndroidTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
//EjemploDialogoCompose()
EjemploDialogoListCompose()
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}Cada vez que hagas un cambio importante, la aplicación se recompila. Una vez cargada, ya puedes ver e interactuar con el resultado real.
Diálogo de confirmación simple
Para un diálogo con título, mensaje y botones de confirmación/cancelación, puedes usar AlertDialog directamente. Su visibilidad se controla a través de un estado booleano, exactamente como vimos antes.
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
AlertDialog(
onDismissRequest = {
// Se ejecuta cuando el usuario toca fuera del diálogo o el botón de atrás
showDialog = false
},
title = { Text("Título del Diálogo") },
text = { Text("Este es un mensaje de ejemplo para el diálogo. ¿Estás seguro de continuar?") },
confirmButton = {
TextButton(
onClick = {
// Acción de confirmación
showDialog = false
}
) {
Text("Confirmar")
}
},
dismissButton = {
TextButton(
onClick = {
// Acción de cancelación
showDialog = false
}
) {
Text("Cancelar")
}
}
)
}
// Botón para mostrar el diálogo
Button(onClick = { showDialog = true }) {
Text("Mostrar Diálogo")
}El resultado es un diálogo funcional y estilizado según los principios de Material Design 3, sin ninguna configuración adicional.
Diálogo con una lista de opciones
Para mostrar una lista de elementos seleccionables dentro de un diálogo, puedes colocar un LazyColumn dentro del parámetro text. LazyColumn es el equivalente en Compose al RecyclerView del sistema de vistas: renderiza únicamente los elementos visibles en pantalla, lo que lo hace eficiente incluso con listas largas.
val items = listOf("Opción 1", "Opción 2", "Opción 3")
AlertDialog(
onDismissRequest = { /* ... */ },
title = { Text("Selecciona una opción") },
text = {
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.clickable {
// Acción al seleccionar un item
println("$item seleccionado")
}
.padding(vertical = 12.dp)
)
}
}
},
confirmButton = { /* ... */ }
)Diálogo completamente personalizado
Si necesitas control total sobre el diseño del diálogo —campos de texto, sliders, imágenes, etc.— puedes usar directamente el composable Dialog y colocar cualquier contenido de Compose en su interior. A diferencia de AlertDialog, Dialog no impone ninguna estructura predefinida: tú decides todo.
Dialog(onDismissRequest = { /* ... */ }) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Diálogo Personalizado", style = MaterialTheme.typography.titleLarge)
// Aquí puedes agregar cualquier composable: sliders, checkboxes, etc.
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { /* ... */ }) {
Text("Cerrar")
}
}
}
}Ejemplo con lista interactiva
Veamos un ejemplo algo más completo que combina estado reactivo con una lista de elementos dentro de un diálogo.
La mecánica es la misma de siempre —remember + mutableStateOf—, pero ahora el contenido del diálogo es una lista dinámica. Cada elemento es clickeable y, al seleccionarlo, imprimimos un mensaje en la consola para confirmar la interacción. En un caso real, aquí iría tu lógica de negocio: filtrar, navegar, guardar una selección, etc.
Puedes verificar qué elemento fue seleccionado abriendo Logcat en Android Studio y filtrando por la etiqueta correspondiente.
@Composable
fun EjemploDialogoListCompose() {
// 1. Definimos el estado (¿se muestra el diálogo?)
var showDialog by remember { mutableStateOf(false) }
val items = listOf("Opción 1", "Opción 2", "Opción 3")
// Botón que activa el diálogo
Button(onClick = { showDialog = true }) {
Text("Eliminar elemento")
}
// 2. Declaramos el Diálogo
if (showDialog) {
AlertDialog(
onDismissRequest = {
// Se ejecuta cuando el usuario toca fuera del diálogo o pulsa "Atrás"
showDialog = false
},
title = {
Text(text = "Confirmar eliminación")
},
text = {
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.clickable {
// Acción al seleccionar un item
println("$item seleccionado")
}
.padding(vertical = 12.dp)
)
}
}
},
confirmButton = {
TextButton(onClick = {
showDialog = false
// Aquí iría tu lógica de borrado
}) {
Text("Confirmar")
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Cancelar")
}
}
)
}
}Diálogos con XML y Kotlin (enfoque legacy)
Aunque Jetpack Compose es el presente y el futuro del desarrollo Android, muchas aplicaciones en producción todavía utilizan el sistema de vistas tradicional. Para este enfoque, la clase recomendada es MaterialAlertDialogBuilder de la biblioteca de Material Components, que proporciona diálogos con el estilo de Material Design sin ninguna configuración adicional.
Asegúrate de tener la dependencia en tu archivo build.gradle.kts:
implementation("com.google.android.material:material:1.12.0")Creando un diálogo básico
Lo primero es crear una instancia del builder. Aquí es buena práctica pararse un momento a pensar si la variable necesitará cambiar o no: si no va a cambiar, usa val; si puede que la reasignes, usa var.
Creamos entonces un AlertDialog.Builder, que es el objeto que nos permite configurar y construir el diálogo paso a paso mediante un patrón de llamadas encadenadas (builder pattern):
val builder: AlertDialog.Builder = AlertDialog.Builder(context)Aquí aparece un concepto importante: el contexto.
El contexto es básicamente una referencia al entorno de la aplicación en ese momento, similar a un this en las clases. En este caso, el diálogo va a vivir dentro de MainActivity, por lo que ese es el contexto que le pasamos. Le da al sistema la información necesaria para saber en qué ventana debe mostrarse el diálogo.
val context = LocalContext.current // <-- Esto obtiene el contexto actual en ComposeLuego establecemos el título, el mensaje y los botones de acción. Finalmente, llamamos a create() para construir el diálogo y a show() para mostrarlo cuando corresponda (por ejemplo, al hacer clic en un botón):
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder
.setMessage("I am the message")
.setTitle("I am the title")
.setPositiveButton("Positive") { dialog, which ->
// Do something.
}
.setNegativeButton("Negative") { dialog, which ->
// Do something else.
}
val dialog: AlertDialog = builder.create()
dialog.show()Diálogo simple de mensaje
El diálogo más básico muestra un título, un mensaje y un único botón de acción. Con Kotlin y el patrón builder, el código resulta muy conciso y legible:
MaterialAlertDialogBuilder(this)
.setTitle("Hola mundo: Título")
.setMessage("Hola mundo: Mensaje.")
.setPositiveButton("OK") { dialog, which ->
// Acción al presionar OK
}
.show()
Diálogo de confirmación
Para un diálogo de sí/no, simplemente añade un botón negativo con setNegativeButton. Llama a dialog.dismiss() dentro del callback si quieres cerrarlo explícitamente.
MaterialAlertDialogBuilder(this)
.setTitle("Confirmación")
.setMessage("¿Estás seguro de que deseas realizar esta acción?")
.setNegativeButton("Cancelar") { dialog, which ->
dialog.dismiss() // Cierra el diálogo
}
.setPositiveButton("Aceptar") { dialog, which ->
// Acción de confirmación
}
.show()

Diálogo de selección simple (lista)
Para mostrar una lista de opciones donde el usuario elige una y el diálogo se cierra automáticamente, usa el método setItems. El índice de la opción seleccionada llega como parámetro which en el callback.
val items = arrayOf("Opción 1", "Opción 2", "Opción 3")
MaterialAlertDialogBuilder(this)
.setTitle("Elige una opción")
.setItems(items) { dialog, which ->
Log.i("Dialog", "Opción seleccionada: ${items[which]}")
}
.show()

Diálogo de selección simple con radio buttons
Una variante del anterior que mantiene la selección resaltada visualmente hasta que el usuario confirma con un botón. Usa setSingleChoiceItems y un índice (checkedItem) para rastrear qué opción está activa.
val items = arrayOf("Opción 1", "Opción 2", "Opción 3")
var checkedItem = 0 // Opción seleccionada por defecto
MaterialAlertDialogBuilder(this)
.setTitle("Elige una opción")
.setSingleChoiceItems(items, checkedItem) { dialog, which ->
checkedItem = which
}
.setPositiveButton("OK") { dialog, which ->
Log.i("Dialog", "Opción final: ${items[checkedItem]}")
}
.show()

Diálogo de selección múltiple (checkboxes)
Para permitir que el usuario seleccione varias opciones simultáneamente, usa setMultiChoiceItems. El array checkedItems actúa como el estado inicial de cada checkbox; el callback te informa en tiempo real qué elemento fue marcado o desmarcado.
val items = arrayOf("Opción 1", "Opción 2", "Opción 3")
val checkedItems = booleanArrayOf(true, false, false) // Selecciones iniciales
MaterialAlertDialogBuilder(this)
.setTitle("Elige tus preferencias")
.setMultiChoiceItems(items, checkedItems) { dialog, which, isChecked ->
Log.i("Dialog", "Opción ${items[which]} marcada: $isChecked")
}
.setPositiveButton("OK") { dialog, which ->
// Acceder a `checkedItems` para ver las selecciones finales
}
.show()

Diálogo personalizado con layout XML
Para diseños complejos que van más allá de lo que ofrece el builder por defecto, puedes inflar un layout XML personalizado usando setView(). Se recomienda usar View Binding para acceder a las vistas del layout de forma segura y sin necesidad de llamar a findViewById.
// 1. Inflar el layout personalizado con View Binding
val binding = DialogCustomLayoutBinding.inflate(layoutInflater)
// 2. Configurar el diálogo
MaterialAlertDialogBuilder(this)
.setTitle("Formulario Personalizado")
.setView(binding.root)
.setPositiveButton("Enviar") { dialog, which ->
val userInput = binding.editText.text.toString()
// Hacer algo con los datos
}
.setNegativeButton("Cancelar", null)
.show()

El siguiente paso: aprende a crear Card Views personalizados en Android Studio.