Content Index
- Dialogs with Jetpack Compose (the modern approach)
- Managing dialog state
- Showing the dialog
- What do remember and mutableStateOf do exactly?
- Preview vs. Emulator
- Simple confirmation dialog
- Dialog with a list of options
- Completely custom dialog
- Example with interactive list
- Dialogs with XML and Kotlin (legacy approach)
- Creating a basic dialog
- Simple message dialog
- Confirmation dialog
- Single choice selection dialog (list)
- Single choice selection dialog with radio buttons
- Multiple choice selection dialog (checkboxes)
- Custom dialog with XML layout
Dialogs in Android are small modal windows that appear on top of the main content to prompt the user to make a decision or enter additional information. They are a fundamental component for user interaction and can be completely customized: from their visual appearance to the action buttons they display.
Continuing our journey through Android components, today it's time for dialogs.
These are those small windows that we can use for multiple situations: showing information to the user, confirming or canceling a critical action, presenting lists of options, or even rendering complete forms. It is a fairly open and flexible element that, when used properly, greatly improves the user experience.
Currently, there are two main approaches to creating dialogs in Android: the modern and recommended Jetpack Compose, and the traditional XML-based view system using the Material Components library.
In this post, we will explore both methods, starting with the modern approach using Jetpack Compose. If you come from the previous chapter, where we learned how to implement the FloatingActionButton in Android Studio and its variants, this next step will feel very natural.
Dialogs with Jetpack Compose (the modern approach)
Jetpack Compose is Google's modern declarative UI toolkit. Creating dialogs with Compose is simpler and more intuitive than with the traditional view system, since all state and logic live in the same place. The main component for this is the AlertDialog composable.
Managing dialog state
Dialogs are slightly more complex components than a button. Unlike buttons, which we simply place on the screen, dialogs need to be activated and deactivated, which involves managing state.
To do this, we create a state variable called showDialog. It is a variable (var) and not a constant (val) because its value will change throughout the component's lifecycle:
var showDialog by remember { mutableStateOf(false) }Two new concepts appear here: remember and mutableStateOf.
If you come from Flutter, Vue, React, or any other reactive framework, this will be familiar to you. It is basically Compose's answer to reactive state management.
If we were to declare a normal variable —like var showDialog = false— and then change it when clicking a button, the UI would not update automatically. For that redraw to happen automatically, we need to wrap the value in a reactive state.
With mutableStateOf, we tell Compose: "hey, this variable is reactive." When its value changes, Compose will recompose the component reading it and will update the interface automatically.
For its part, remember ensures that this state is preserved across recompositions. Without remember, the state would reset every time Compose redrew the component, losing the previous value.
Showing the dialog
The visibility logic is simple: we use a standard Kotlin conditional. If showDialog is true, we display the dialog; if it is false, nothing is rendered.
The onDismissRequest parameter is the callback executed when the user taps outside the dialog or presses the back button. The most common action is to close it by setting showDialog back to false.
Inside the AlertDialog, we define its main parts:
title: the title of the dialog.text: the body of the message.confirmButton: the confirmation button.dismissButton: the cancellation button.
In both buttons, we close the dialog on click, although the actual business logic will depend on what you need to do: delete an item, confirm a critical operation, 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()
}
}What do remember and mutableStateOf do exactly?
In short:
rememberallows the state to be preserved across recompositions. Without it, the value would reset every time Compose redrew the component.mutableStateOfcreates an observable state: when its value changes, Compose knows it must recompose the composables reading it.
When we change the value of showDialog, Compose detects the change, recomposes the component, and updates the UI.
If the value changes to true, the dialog appears. If it returns to false, it disappears.
Compose can redraw the screen many times, and that is completely normal. What matters is not the value itself, but that it is wrapped in a reactive state so Compose can track its changes.
Preview vs. Emulator
Android Studio's @Preview is only used to see a static mockup of the UI. It has no real functionality nor does it execute business logic.
To see the actual behavior of the dialog —appearing when pressing the button and disappearing upon confirming or canceling— you need to run the app on an emulator or physical device.
Click the Play button, select the emulator, and wait for it to boot. In MainActivity, remove or comment out whatever you don't need and call the dialog composable you created:
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)
)
}
}
}
}
}Every time you make a significant change, the application recompiles. Once loaded, you can now view and interact with the real result.
Simple confirmation dialog
For a dialog with a title, message, and confirm/cancel buttons, you can use AlertDialog directly. Its visibility is controlled through a boolean state, exactly as we saw before.
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")
}The result is a functional dialog styled according to Material Design 3 principles, without any extra configuration.
Dialog with a list of options
To show a list of selectable items inside a dialog, you can place a LazyColumn inside the text parameter. LazyColumn is Compose's equivalent to the view system's RecyclerView: it renders only the items visible on screen, making it efficient even with long lists.
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 = { /* ... */ }
)Completely custom dialog
If you need full control over the dialog's design —text fields, sliders, images, etc.— you can directly use the Dialog composable and place any Compose content inside it. Unlike AlertDialog, Dialog does not impose any predefined structure: you decide everything.
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")
}
}
}
}Example with interactive list
Let's look at a slightly more complete example that combines reactive state with a list of items inside a dialog.
The mechanics are the same as always —remember + mutableStateOf—, but now the content of the dialog is a dynamic list. Each item is clickable and, upon selecting it, we print a message in the console to confirm the interaction. In a real scenario, your business logic would go here: filtering, navigating, saving a selection, etc.
You can verify which item was selected by opening Logcat in Android Studio and filtering by the corresponding tag.
@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")
}
}
)
}
}Dialogs with XML and Kotlin (legacy approach)
Although Jetpack Compose is the present and future of Android development, many production applications still use the traditional view system. For this approach, the recommended class is MaterialAlertDialogBuilder from the Material Components library, which provides dialogs styled with Material Design out of the box without extra configuration.
Make sure you have the dependency in your build.gradle.kts file:
implementation("com.google.android.material:material:1.12.0")Creating a basic dialog
First, create an instance of the builder. It is good practice here to pause and think whether the variable will need to change or not: if it won't change, use val; if you might reassign it, use var.
We then create an AlertDialog.Builder, which is the object that allows us to configure and build the dialog step-by-step using a chained calls pattern (builder pattern):
val builder: AlertDialog.Builder = AlertDialog.Builder(context)An important concept appears here: the context.
Context is basically a reference to the application environment at that moment, similar to a this in classes. In this case, the dialog will live inside MainActivity, so that is the context we pass to it. It provides the system with the necessary information to know in which window the dialog should be displayed.
val context = LocalContext.current // <-- This gets the current context in ComposeNext, we set the title, message, and action buttons. Finally, we call create() to build the dialog and show() to display it when appropriate (for example, when clicking a button):
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()Simple message dialog
The most basic dialog displays a title, a message, and a single action button. With Kotlin and the builder pattern, the code becomes very concise and readable:
MaterialAlertDialogBuilder(this)
.setTitle("Hola mundo: Título")
.setMessage("Hola mundo: Mensaje.")
.setPositiveButton("OK") { dialog, which ->
// Acción al presionar OK
}
.show()
Confirmation dialog
For a yes/no dialog, simply add a negative button with setNegativeButton. Call dialog.dismiss() inside the callback if you want to close it explicitly.
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()

Single choice selection dialog (list)
To display a list of options where the user picks one and the dialog closes automatically, use the setItems method. The index of the selected option arrives as the which parameter in the 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()

Single choice selection dialog with radio buttons
A variant of the previous one that keeps the selection visually highlighted until the user confirms with a button. Use setSingleChoiceItems and an index (checkedItem) to track which option is active.
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()

Multiple choice selection dialog (checkboxes)
To allow the user to select multiple options simultaneously, use setMultiChoiceItems. The checkedItems array acts as the initial state of each checkbox; the callback informs you in real time which element was checked or unchecked.
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()

Custom dialog with XML layout
For complex layouts beyond what the default builder provides, you can inflate a custom XML layout using setView(). It is recommended to use View Binding to access the layout views safely and without needing to call 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()

Next step: learn how to create custom Card Views in Android Studio.