Ejemplo práctico 9: Menú con DropdownMenu en BottomAppBar

Objetivo

Este ejemplo es una variante del anterior. Se sustituirá la TopAppBar con un menú por una BottomAppBar, evaluando la selección del usuario mediante un único callback, comprobando la respuesta recibida y actuando en consecuencia. Debes tener en cuenta que en Compose los métodos no devuelven valores, de ahí el uso de callbacks. Se mantendrá la misma estructura de sealed class, añadiendo label e icon.

Recursos en string.xml

Como en la versión anterior, se tratará de evitar lo máximo posible el hardcoded text, aunque en ocasiones, verás que se omite por razones didácticas.

 1<resources>
 2    <string name="app_name">ExampleT2_8</string>
 3
 4    <string name="txt_welcome">Selecciona una opción del menú</string>
 5
 6    <string name="txt_option_title">Más opciones</string>
 7
 8    <string name="txt_option_share">Compartir</string>
 9    <string name="txt_option_save">Guardar</string>
10    <string name="txt_option_logout">Cerrar sesión</string>
11
12    <string name="txt_share">Has seleccionado la opción <b>Compartir</b>.</string>
13    <string name="txt_save">Has seleccionado la opción <b>Guardar</b>.</string>
14    <string name="txt_logout">Has seleccionado la opción <b>Cerrar sesión</b>.</string>
15</resources>

Sealed Class

Para simplificar el código, se creará la siguiente sealed class en un fichero a parte, lo que permite reducir las evaluaciones para este caso. Como se ha comentado, se añadirán dos nuevas propiedades a la clase label e icon.

1sealed class OpcionMenu(val label: String, val icon: ImageVector) {
2    object Compartir : OpcionMenu("Compartir", Icons.Default.Share)
3    object Guardar : OpcionMenu("Guardar", Icons.Default.Add)
4    object Logout : OpcionMenu("Cerrar sesión", Icons.AutoMirrored.Filled.ExitToApp)
5
6    companion object {
7        val todas = listOf(Compartir, Guardar, Logout)
8    }
9}

Compose BottomAppBarConMenu

Supón que quieres reutilizar este componente en más de una vista, para eso se creará este componente en un fichero separado, por ejemplo, Utils.kt. Esta versión está mejorada con respecto al ejemplo anterior, se crea un bucle para mostrar las opciones que se vayan añadiendo en la sealed class.

 1@Composable
 2fun BottomAppBarConMenu(
 3    onOpcionSeleccionada: (OpcionMenu) -> Unit
 4) {
 5    var expanded by remember { mutableStateOf(false) }
 6
 7    BottomAppBar(
 8        actions = {
 9            IconButton(onClick = { expanded = !expanded }) {
10                Icon(Icons.Default.MoreVert, contentDescription = "Menú inferior")
11            }
12
13            DropdownMenu(
14                expanded = expanded,
15                onDismissRequest = { expanded = false }
16            ) {
17                OpcionMenu.todas.forEach { opcion ->
18                    DropdownMenuItem(
19                        text = { Text(opcion.label) },
20                        leadingIcon = { Icon(opcion.icon, contentDescription = null) },
21                        onClick = {
22                            expanded = false
23                            onOpcionSeleccionada(opcion)
24                        }
25                    )
26                }
27            }
28        }
29    )
30}

Resultado de la MainActivity

Ahora, la actividad principal tendrá un aspecto más limpio al hacer uso de la sealed class y el componente creado en un fichero a parte.

 1class MainActivity : ComponentActivity() {
 2    override fun onCreate(savedInstanceState: Bundle?) {
 3        super.onCreate(savedInstanceState)
 4        enableEdgeToEdge()
 5        setContent {
 6            ExampleT2_9Theme {
 7                PantallaPrincipal()
 8            }
 9        }
10    }
11}
12
13@Preview(showBackground = true)
14@Composable
15fun PantallaPrincipal() {
16    val context = LocalContext.current
17    var mensaje by remember { mutableStateOf(context.getString(R.string.txt_welcome)) }
18
19    Scaffold(
20        bottomBar = {
21            BottomAppBarConMenu { opcion ->
22                mensaje = when (opcion) {
23                    is OpcionMenu.Compartir -> context.getString(R.string.txt_share)
24                    is OpcionMenu.Guardar -> context.getString(R.string.txt_save)
25                    is OpcionMenu.Logout -> context.getString(R.string.txt_logout)
26                }
27            }
28        },
29        modifier = Modifier.fillMaxSize()
30    ) { innerPadding ->
31        Box(
32            modifier = Modifier
33                .fillMaxSize()
34                .padding(innerPadding),
35            contentAlignment = Alignment.Center
36        ) {
37            Text(mensaje)
38        }
39    }
40}

Como puedes ver, BottomAppBar permite el uso de actions igual que la TopAppBar. También es posible implementar este menú utilizando la sección floatingActionButton y utilizando FloatingActionButton, y se puede combinar ambas barras (topBar y bottomBar) en el mismo Scaffold.