Navegando por la aplicación (2ª parte)

Navegando por la aplicación (2ª parte)

En esta entrada, que es una continuación de Navegando entre fragmentos, vamos a ver cómo facilitar al usuario otras maneras de navegar por una aplicación, y concretamente, partiendo de la aplicación Test CI que vimos en esa entrada, vamos a añadir nuevas posibilidades para moverse entre fragmentos. Para ello vamos a presentar el contenido secuenciado en tres pasos a modo de objetivos:
  1. Añadir un botón a la barra de la aplicación.
  2. Crear un menú de opciones.
  3. Crear un cajón de navegación.

1. Añadir un botón Up a la barra de la aplicación

Usualmente en la barra de la aplicación encontramos en el centro un título descriptivo acerca de la aplicación o la pantalla concreta, a su derecha un botón con el icono de tres puntos verticales para acceder al menú de opciones y, dependiendo de la pantalla, el botón Arriba (the Up button) a la izquierda en forma de flecha apuntando hacia la izquierda.

En cuanto al título, para establecer desde un fragmento el título que aparece en la barra de la aplicación podemos invocar supportActionBar.title; por ejemplo en el caso del fragmento de inicio, TitleFragment.kt, dentro de onCreate, sería:
// requiere import androidx.appcompat.app.AppCompatActivity
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.app_name)
Repetimos lo anterior para todos los fragmentos (excepto en TestFragment.kt puesto que ya lo tiene) asignando a cada uno su respectivo recurso de texto almacenado en values/strings.xml.

En Navegando entre fragmentos ya configuramos el botón Atrás del dispositivo para navegar a la pantalla que nos interesaba en cada caso, pero además podemos añadir un botón Up en la barra de la aplicación con un comportamiento similar. Una diferencia importante entre los botones Arriba y Atrás es que el primero es dependiente de la aplicación y navega dentro de ella sin salir de la aplicación, mientras que el segundo es dependiente del dispositivo y navega hacia atrás en la pila posterior de destinos o back stack del sistema. Y en nuestra aplicación nos interesa incluir un botón Up que aparezca en todas las pantalla excepto en la pantalla de inicio (la pantalla de título: fragment_title.xml) puesto que esta pantalla está en la parte superior de la jerarquía de la aplicación.

Recuerda que en su momento ya incluimos las dependencias de navegación a los archivos de Gradle (revisar la entrada Navegando entre fragmentos), y los componentes de navegación incluyen una biblioteca llamada NavigationUI que permite que el controlador de navegación se integre con la barra de aplicaciones para implementar el comportamiento del botón Up. Vamos a utilizarlo.

En el archivo MainActivity.kt, dentro del método onCreate agregamos el código para localizar el objeto del controlador de navegación (recuerda que en activity_main.xml se identificó el fragmento como myNavHostFragment para crear el NavHostFragment):
// requiere import androidx.navigation.findNavController
val navController = this.findNavController(R.id.myNavHostFragment)
Inmediatamente después, vinculamos el controlador de navegación a la barra de la aplicación:
// requiere import androidx.navigation.ui.NavigationUI
NavigationUI.setupActionBarWithNavController(this, navController)
Y ahora, fuera del método onCreate, sobrescribimos el método onSupportNavigateUp para invocar navigationUp en el controlador de navegación:
override fun onSupportNavigateUp(): Boolean {
    val navController = this.findNavController(R.id.myNavHostFragment)
    return navController.navigateUp()
}
Ejecutamos la aplicación para comprobar que el botón Up aparece en la barra de la aplicación de todas las pantallas excepto en la pantalla de inicio, y que al pulsarlo siempre nos lleva a la pantalla de inicio independientemente de la pantalla donde nos encontremos. Además cada pantalla aparece con su correspondiente título en la barra de la aplicación. Primer objetivo conseguido, a por el segundo.

2. Crear un menú de opciones

En Android se accede al menú de opciones desde el botón de tres puntos verticales situado en la barra de la aplicación. Vamos a incorporarlo a nuestra aplicación y en el menú de opciones vamos a añadir un elemento (Acerca de) para que, cuando el usuario lo pulse, la aplicación navegue al fragmento AboutFragment y muestre la información que contiene este fragmento.

Primero, con el botón New Destination agregamos el fragmento aboutFragment al gráfico de navegación:



Después debemos agregar el recurso de menú de opciones: desde la carpeta res, New -> Android Resource File, y en la nueva ventana que se abre ajustamos el nombre (options_menu) y el tipo (Menu):



Abrimos menu/options_menu.xml en vista Design para ver el editor de diseño. Desde la paleta de elementos arrastramos Menu Item al árbol de componentes bajo el elemento menu:



Seleccionamos el nuevo Item y desde el panel de atributos ajustamos el ID en aboutFragment (asegúrate de que este valor es el mismo que el valor de la ID del fragmento en el gráfico de navegación) y el título en @string/about.

Ahora falta incorporle un controlador onClick. En el archivo TitleFragment.kt, dentro del método onCreateView y por supuesto antes de return, invocamos el método setHasOptionsMenu con el argumento true:
setHasOptionsMenu(true)
Ahora, fuera y después del método onCreateView, sobrescribimos el método onCreateOptionsMenu y, en él, agregamos el menú de opciones y activamos (inflamos) el archivo de recursos del menú:
// import android.view.*
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
    super.onCreateOptionsMenu(menu, inflater)
    inflater?.inflate(R.menu.options_menu, menu)
}
Sobrescribimos el método onOptionsItemSelected para seleccionar la acción adecuada cuando se pulsa el elemento del menú, esto es, navegar hasta el fragmento que tiene la misma identificación (id) que el elemento del menú:
// import androidx.navigation.ui.NavigationUI
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
    return NavigationUI.onNavDestinationSelected(item!!,
        view!!.findNavController())
        || super.onOptionsItemSelected(item)
}
Ejecutamos en el emulador para comprobar que ahora en la barra de la aplicación de la pantalla de inicio aparece, a la derecha, el botón de tres puntos verticales, y al pulsarlo se muestra un menú con el ítem Acerca de, que nos lleva a la pantalla del fragmento correspondiente. ¡Segundo objetivo conseguido!

3. Crear un cajón de navegación

El cajón de navegación (the navigation drawer) forma parte de Material (Material library o Material Components for Android library, biblioteca utilizada para implementar modelos siguiendo las reglas Material Design de Google) y consiste en un panel que se desliza desde el borde de la pantalla y que generalmente contiene un encabezado y un menú. En dispositivos del tamaño de un smartphone este cajón está oculto cuando no se usa, y suele mostrarse con dos acciones del usuario: con un movimiento de deslizamiento desde un borde de la pantalla hacia el otro (en nuestra aplicación haremos que aparezca cuando el usuario deslice de izquierda a derecha) y pulsando el botón de tres líneas horizontales (nav drawer button o hamburger icon) que está en la barra de la aplicación.

En nuestra aplicación el cajón de navegación contendrá dos elementos de menú, uno como el recién creado item Acerca de y un segundo elemento que apuntará al fragmento (InfoFragment) con información sobre el funcionamiento de la aplicación.

Primero debemos agregar la Material library al proyecto. Para ello, en el archivo build.gradle (Module:app) añadimos la dependencia correspondiente:
dependencies {
    ...
    implementation "com.google.android.material:material:$supportlibVersion"
}
Y sincronizamos el proyecto.

En el gráfico de navegación añadimos el fragmento infoFragment (ID: infoFragment):



Después, para crear el cajón de navegación, desde la carpeta res -> New -> Android Resource File y ajustamos los valores File name: navdrawer_menu y Resource type: Menu.

En menu/navdrawer_menu.xml (vista Design) agregamos dos elementos Menu Item desde la paleta de elementos hasta el árbol de componentes, bajo el elemento menu. Para el primer Item establecemos id: infoFragment (la ID del fragmento en el gráfico de navegación), como título @string/info y como icono una imagen desde res/drawable (@drawable/ic_code). Para el segundo Item ajustamos la id a aboutFragment (comprueba que coincide con la id en el gráfico de navegación), el título a @string/about y el icono a @drawable/ic_mid_mapping.

Abrimos layout/activity_main.xml en vista Text, y colocamos todos los elementos dentro de un DrawerLayout como elemento inmediatamente después del layout raíz, envolviendo el LinearLayout:
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <fragment
                android:id="@+id/myNavHostFragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/navigation" />

        </LinearLayout>

    </androidx.drawerlayout.widget.DrawerLayout>

</layout>
Ahora añadimos el cajón, que es un NavigationView que usa el navdrawer_menu que definimos antes, después del cierre de la etiqueta LinearLayout:
<com.google.android.material.navigation.NavigationView
    android:id="@+id/navView"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/navdrawer_menu" />
Vemos que Android Studio nos advierte de un error, ya que el recurso de diseño layout/nav_header no existe. Se trata del diseño del encabezado del cajón, que queremos que sea una imagen de la aplicación. Pulsamos sobre el error y con Alt + Enter (o desde el icono de la bombilla) lo creamos con androidx.constraintlayout.widget.ConstraintLayout como elemento raíz (y la advertencia del error desaparecerá):



Y le añadimos una ImageView quedando de esta manera:
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navHeader"
    android:layout_width="match_parent"
    android:layout_height="@dimen/image_header_height"
    android:background="@color/fondoazul"
    android:padding="@dimen/button_padding"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">

    <ImageView
        android:id="@+id/navHeaderImage"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="@dimen/horizontal_margin"
        android:layout_marginTop="@dimen/top_margin"
        android:layout_marginEnd="@dimen/horizontal_margin"
        android:layout_marginBottom="@dimen/bottom_margin"
        android:scaleType="fitCenter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_problem_solving" />

</androidx.constraintlayout.widget.ConstraintLayout>
Hasta ahora hemos creado los elementos del menú para el cajón de navegación y el diseño del propio cajón, pero necesitamos conectar el cajón al controlador de navegación para que cada elemento navegue a su correspondiente fragmento. Abrimos MainActivity.kt y en onCreate permitimos al usuario mostrar el cajón de navegación llamando al método setupWithNavController:
NavigationUI.setupWithNavController(binding.navView, navController)
Si en principio no reconoce la referencia a navView que acabamos de crear en activity_main.xml, podemos solucionarlo desde Build -> Clean Project y después Build -> Rebuild Project. Ejecutamos la aplicación y hacemos un movimiento de deslizamiento desde el borde izquierdo para mostrar el cajón de navegación, y comprobamos que cada item navega a su destino correcto:



Funciona, pero deberiamos también mostrar el cajón de navegación desde el botón de tres líneas de la barra de la aplicación. Para hacerlo, en MainActivity.kt, justo antes del método onCreate, declaramos una variable con el modificador lateinit (para permitir retrasar su inicialización) para representar el diseño del cajón (requiere import androidx.drawerlayout.widget.DrawerLayout):
private lateinit var drawerLayout: DrawerLayout
Y dentro del método onCreate la inicializamos después de que se haya inicializado la variable de enlace:
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

drawerLayout = binding.drawerLayout
Y la agregamos como tercer parámetro al método setupActionBarWithNavController:
// NavigationUI.setupActionBarWithNavController(this, navController)
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
Ahora editamos el método onSupportNavigateUp para devolver, en lugar de navController, NavigationUI con el método navigateUp pasándole el controlador de navegación y el diseño del cajón:
override fun onSupportNavigateUp(): Boolean {
    val navController = this.findNavController(R.id.myNavHostFragment)
    // return navController.navigateUp()
    return NavigationUI.navigateUp(navController, drawerLayout)
}
Ejecutamos la aplicación en el emulador y comprobamos que en la pantalla de inicio, en la barra de la aplicación, a la izquierda tenemos el botón de tres líneas que abre el cajón de navegación (que también podemos abrir desde cualquier pantalla deslizando de izquierda a derecha).

Finalmente hemos creado distintas opciones de navegación en la aplicación. En resumen, el usuario puede avanzar a través de la aplicación según el acierto o no de sus respuestas, puede volver en cualquier momento a la pantalla de inicio usando el botón Arriba, y puede acceder a la pantalla Acerca de desde el menú de opciones o bien desde el cajón de navegación que se puede abrir con un gesto de deslizamiento o con el botón de la barra de la aplicación en la pantalla de inicio; además, al pulsar en el botón Atrás del dispositivo regresa a pantallas anteriores de una manera que tiene sentido para la aplicación (no necesariamente a la última pantalla visitada). ¡Objetivo conseguido: La aplicación incluye rutas de navegación robustas y lógicas que son intuitivas para el usuario!

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos