Ejemplo práctico 8: Menú básico en TopAppBar con devolución de selección vía callback

Objetivo

Este ejemplo trata de plantear una posible solución a problemas que pueden plantearse durante el desarrollo de aplicaciones móviles. La idea es crear un componente para montar una TopAppBar con un menú, evaluando la selección del usuario mediante un único callback, comprobando la respuesta producida y actuando en consecuencia. Debes tener en cuenta que en Compose los métodos no devuelven valores, de ahí el uso de callbacks.

Recursos en string.xml

Tratará de evitarse 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.

 1sealed class OpcionMenu {
 2    object Compartir : OpcionMenu()
 3    object Guardar : OpcionMenu()
 4    object Logout : OpcionMenu()
 5
 6    override fun toString(): String {
 7        return when (this) {
 8            Compartir -> "Compartir"
 9            Guardar -> "Guardar"
10            Logout -> "Cerrar sesión"
11        }
12    }
13}

Compose TopBarConMenu

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.

 1@OptIn(ExperimentalMaterial3Api::class)
 2@Composable
 3fun TopBarConMenu(
 4    onOpcionSeleccionada: (OpcionMenu) -> Unit
 5) {
 6    var expanded by remember { mutableStateOf(false) }
 7    val context = LocalContext.current
 8
 9    TopAppBar(
10        title = { Text("TopAppBar con Menú") },
11        colors = topAppBarColors(
12            containerColor = MaterialTheme.colorScheme.primaryContainer,
13            titleContentColor = MaterialTheme.colorScheme.primary,
14        ),
15        actions = {
16            IconButton(onClick = { expanded = true }) {
17                Icon(Icons.Default.MoreVert, contentDescription = context.getString(R.string.txt_option_title))
18            }
19            DropdownMenu(
20                expanded = expanded,
21                onDismissRequest = { expanded = false }
22            ) {
23                DropdownMenuItem(
24                    text = { Text(context.getString(R.string.txt_option_share)) },
25                    onClick = {
26                        expanded = false
27                        onOpcionSeleccionada(OpcionMenu.Compartir)
28                    }
29                )
30                DropdownMenuItem(
31                    text = { Text(context.getString(R.string.txt_option_save)) },
32                    onClick = {
33                        expanded = false
34                        onOpcionSeleccionada(OpcionMenu.Guardar)
35                    }
36                )
37                DropdownMenuItem(
38                    text = { Text(context.getString(R.string.txt_option_logout)) },
39                    onClick = {
40                        expanded = false
41                        onOpcionSeleccionada(OpcionMenu.Logout)
42                    }
43                )
44            }
45        }
46    )
47}

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
 6        setContent {
 7            ExampleT2_8Theme {
 8                PantallaPrincipal()
 9            }
10        }
11    }
12}
13
14@Preview(showBackground = true)
15@Composable
16fun PantallaPrincipal() {
17    val context = LocalContext.current
18    var mensaje by remember { mutableStateOf(context.getString(R.string.txt_welcome)) }
19
20    Scaffold(
21        topBar = {
22            TopBarConMenu { opcion ->
23                mensaje = when (opcion) {
24                    is OpcionMenu.Compartir -> context.getString(R.string.txt_share)
25                    is OpcionMenu.Guardar -> context.getString(R.string.txt_save)
26                    is OpcionMenu.Logout -> context.getString(R.string.txt_logout)
27                }
28            }
29        },
30        modifier = Modifier.fillMaxSize()
31    ) { innerPadding ->
32        Box(
33            modifier = Modifier
34                .padding(innerPadding)
35                .fillMaxSize(),
36            contentAlignment = Alignment.Center
37        ) {
38            Text(
39                text = mensaje,
40                fontSize = 18.sp
41            )
42        }
43    }
44}

El uso de este esquema permite el tipado seguro, evitando así errores de escritura en las cadenas, es escalable, está integrado con when lo que permite una evaluación exhaustiva y permite la reutilización, ya que la acción se realizará en el when, y no en el método encargado de montar el menú.

Puede ser más óptimo, por ejemplo, añadiendo propiedades como label o icon dentro de la sealed class.