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.