15 - Cajon de navegación (drawer)

Los cajones de navegación modal bloquean la interacción con el resto del contenido de una aplicación, hasta que se elige una opción o se cierra.

Se emplean principalmente en dispositivos móviles donde el espacio de la pantalla es limitado, lo más común es que se abran presionando el botón de hamburguesa de la parte superior izquierda.

Problema

Confeccionar una aplicación con tres pantalla que muestren datos generales de 3 países. Disponer un cajón de navegación que nos permita cambiar el país seleccionado.

Crear un proyecto llamado Compose17, la interfaz visual de la misma cuando el cajón de navegación esté abierto debe ser similar a:

cajón de navegación con Jetpack Compose

Agregamos la dependencia de Navigation Compose en el archivo Gradle:

dependencies {

    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation("androidx.navigation:navigation-compose:2.4.0-alpha06") 
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

El código fuente que tenemos que implementar es:

package com.tutorialesprogramacionya.compose17


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PantallaNavegacion()
        }
    }
}

@Composable
fun PantallaNavegacion() {
    val navController = rememberNavController()
    val estadoDrawer = rememberScaffoldState()
    val coroutineScope = rememberCoroutineScope()
    Scaffold(
        scaffoldState = estadoDrawer,
        drawerContent = {
            Column() {
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallaargentina")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Argentina",fontSize = 30.sp)
                }
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallabrasil")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Brasil",fontSize = 30.sp)
                }
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallacolombia")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Colombia",fontSize = 30.sp)
                }
            }
        },
        drawerShape = RoundedCornerShape(topEnd = 23.dp, bottomEnd = 23.dp),
        topBar = {
            TopAppBar(
                title = {
                    Text(text = "Países Latinoamericanos")
                },
                navigationIcon = {
                    IconButton(onClick = {
                        coroutineScope.launch { estadoDrawer.drawerState.open() }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = null)
                    }
                }
            )
        },
    ) {
        NavHost(navController = navController, startDestination = "pantallaargentina") {
            composable("pantallaargentina") {
                PantallaArgentina()
            }
            composable("pantallabrasil") {
                PantallaBrasil()
            }
            composable("pantallacolombia") {
                PantallaColombia()
            }
        }
    }
}


@Composable
fun PantallaArgentina() {
    val scroll = rememberScrollState(0)
    Text(
        text = "Argentina, oficialmente República Argentina,",
        modifier = Modifier.verticalScroll(scroll)
    )
}

@Composable
fun PantallaBrasil() {
    val scroll = rememberScrollState(0)
    Text(
        text = "Brasil, oficialmente República Federativa de Brasil",
        modifier = Modifier.verticalScroll(scroll)
    )
}

@Composable
fun PantallaColombia() {
    val scroll = rememberScrollState(0)
    Text(
        text = "Colombia, oficialmente República de Colombia",
        modifier = Modifier.verticalScroll(scroll)
    )
}



Para resolver el problema debemos definir dos variables para mantener el estado y administración de nuestro menú de cajón:

    val estadoDrawer = rememberScaffoldState()
    val coroutineScope = rememberCoroutineScope()

Por otro lado debemos inicializar el parámetro scaffoldState con la variable de estado 'estadoDrawer', y debemos implementar la función lambda donde creamos la interfaz visual de nuestro menú de cajón y asignarla al parámetro drawerContent:

    Scaffold(
        scaffoldState = estadoDrawer,
        drawerContent = {
            Column() {
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallaargentina")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Argentina",fontSize = 30.sp)
                }
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallabrasil")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Brasil",fontSize = 30.sp)
                }
                Row(
                    horizontalArrangement = Arrangement.Start,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp)
                        .clickable {
                            navController.navigate("pantallacolombia")
                            coroutineScope.launch { estadoDrawer.drawerState.close()  }
                        }) {
                    Image(imageVector = Icons.Filled.Info, contentDescription = null)
                    Spacer(modifier = Modifier.width(15.dp))
                    Text(text = "Colombia",fontSize = 30.sp)
                }
            }
        },
        drawerShape = RoundedCornerShape(topEnd = 23.dp, bottomEnd = 23.dp),

La apertura del menú de cajón se dispara cuando el usuario selecciona el ícono de hamburguesa que se encuentra en la barra de la aplicación, en la parte izquierda:

        topBar = {
            TopAppBar(
                title = {
                    Text(text = "Países Latinoamericanos")
                },
                navigationIcon = {
                    IconButton(onClick = {
                        coroutineScope.launch { estadoDrawer.drawerState.open() }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = null)
                    }
                }
            )
        },

Este proyecto lo puede descargar en un zip desde este enlace: Compose17.zip