Android Navigation – Java

En esta entrada vamos a presentar el Componente Navigation de Android, que básicamente nos va a hacer la vida más fácil a la hora de programar transiciones entre pantallas (fragments básicamente) de nuestra aplicación. Vamos a construir una aplicación muy sencilla donde la navegación por la misma la vamos a gestionar con fragments, pero en vez de programar nosotros las transiciones entre los fragments, lo haremos usando el componente Navigation de Android. Esta app es una adaptación de este codelab de Android basado en el código adaptado a Java de codelab en Java.

Gráfico de Navegación XML editado en Android Studio

A lo largo del tiempo, los desarrolladores se las iban ingeniando para crear sus propias clases que permitieran realizar una navegación más cómoda desde el punto de vista del programador entre las activities y los fragments de sus aplicaciones. Finalmente Android desarrolló en componente Navigation, que desde el core de Android soluciona y facilita esta tarea enormemente, abstrayendo al programador de la gestión de fragments para navegar entre ellos.

Entre los beneficios que incluye este componente, el Navigation Component están los siguientes:

  • Manejo automático de las transactions en los fragments.
  • Manejo correcto de la navegación alante y atrás.
  • Comportamientos por defecto para las animaciones y transiciones
  • Deep linking o acceso a un punto de entrada en el flujo del stack.
  • Implementación de patrones de interfaz de usuario para la navegación, como Navigation Drawers y Bottom Navigation con muy poco trabajo adicional.
  • Paso de parámetros y objetos entre fragment type-safe (comprobados en tiempo de compilación)
  • Herramientas de Android Studio para visualizar y editar el flujo de navegación de la aplicación.
Aplicación BasicNavigation

En la animación tenéis cómo va a ejecutarse nuestra aplicación. Las pantallas que aparecen en el gráfico de navegación son las pantallas que estamos viendo ejecutarse en la aplicación. Como vemos, el gráfico de navegación define cómo se va a navegar por la aplicación.

Construiremos la aplicación que se muestra en el gif animado, pero vamos a partir de una aplicación con algo de código y recursos ya creados, donde deberemos completar los Todo Steps para darle la funcionalidad.

La aplicación previa GitHub Base (donde todos los Todo están sin hacer) la podéis descargar a continuación.

La aplicación GitHub StepByStep, tiene un commit para cada Todo Step propuesto.

Al final de este artículo estará el código de la aplicación GitHub Completa para vuestra comprobación.

La aplicación base, BasicNavigationBase ya incorpora una serie de elementos:

  • Gráfico de navegación (.xml)
  • Gestión de la navegación por Destino y por Acción (Navigation by destination and action)
  • Animaciones de transición
  • Menu navigation, bottom navigation, y menu drawer navigation
  • Paso de argumentos Type safe entre fragments

La aplicación requiere de:

  • Android Studio 3.2 o superior
  • Emulator o dispositivo ejecutanto API 14+

Visión general : Navigation

El componente Navigation consiste en tres partes fundamentales que trabajan en conjunto. Estas son:

  • Navigation Graph (New XML resource) – Este es un recurso (.xml) que contiene toda la información relacionada con la navegación en una ubicación centralizada. Esto incluye todas las pantallas de tu aplicación, que vamos a nombrar como destinos (destinations) y todas las posibles rutas que podrá seguir el usuario para ir de una pantalla a otra. (Las pantallas serán fragments).
  • NavHostFragment (Layout XML view) – Esto es un Widget especial que se va a añadir al Layout de tu Activity principal, y es donde se van a renderizar los fragments incluidos en la navegación. Es el lugar donde se van a mostrar los distintos destinations de tu navegación.
  • NavController (Kotlin/Java object) – Este es un objeto que mantiene la pista de la posición actual el usuario en el grafo de navegación, es decir la pantalla activa. Además gestiona el cambio de un destino por otro mostrando el contenido en el NavHostFragment de manera automática cuando navegas por la aplicación.

Cuando navegas, usarás el objeto NavControler, diciendole a donde quieres ir o bien que path vas a seguir desde donde estás. Para ello el NavControler deberá conocer el NavigationGraph y además deberá conocer cual es el NavHostFragment donde pintar el fragment al que quieres ir. También aplicará las animaciones de la transición entre fragments que estarán definidas en tu NafigationGraph (recurso .xml)

Esta es la idea básica. Vamos a ver cómo empezar….

Introducción al Navigation Graph

Destinations

El componente Navigation introduce el concepto de destination. Un destino (destination) es un lugar al que puedes navegar cuando usas tu aplicación. Normalmente un fragment, pero puede ser también una activity.

Navigation Graph

Navigation Graph inicial, añadiremos destinations y actions

Un navigation graph es un nuevo tipo de recurso que define todas las rutas que un usuario puede seguir en nuestra aplicación. Muestra de manera visual todos los destinos que pueden ser alcanzados desde otro destino. Android Studio muestra el gráfico en un editor, el Navigation Editor. En la figura inicial se muestra una imagen del editor gráfico con los destinos que vamos a utilizar.

En el Navigation Graph, las pantallas son destinos (destinations) y las flechas son acciones (Actions). Podremos navegar de una pantalla a otra usando dos técnicas distintas:

  1. Navegando a un destino, es decir, indicamos el destino (pantalla – fragment o activity) a donde navegar
  2. Indicando una acción.

Cada Destination tiene definidas una serie de acciones que nos llevan a otro destination. Estando en un destination, ejecutando una action nos vamos a otro destination.

Si te fijas en la animación, en la pantalla inicial (home) tenemos dos botones, el primero utiliza la técnica de navegación por destino y el segundo utiliza la técnica navegación por acción.

Dependencias

Para poder trabajar con el componente Navigation, debemos incorporar a nuestro proyecto las siguientes dependencias.

  • navigation-fragment:<version>
  • navigation-ui:<version>
    //Navigation
    implementation 'androidx.navigation:navigation-fragment:2.4.1'
    implementation 'androidx.navigation:navigation-ui:2.4.1'
Dependencias para incorporar Navigation a nuestro proyecto.

Para añadir dependencias al proyecto iremos al menú Build -> Edit Libraries and Dependencies… O bien File->Project Structure

Una vez abierto, seleccionamos la pestaña Dependencies y a la derecha vemos un menu vertical, donde pulsaremos + para añadir una librería (Library dependency), aperecerá una lista con las librerías que están disponibles para la versión de compilación del Sdk que estemos usando.

En el buscador podemos escribir androidx.navigation y darle a buscar.

Seleccionamos las dos librerías mencionadas en su ultima versión.

Trabajando con Destinations

Primero tenemos que abrir el Navigation Graph. Vemos que hay un recurso de tipo navigation, mobile_navigation.xml , lo abrimos y seleccionamos la vista de diseño. Recordar que tenemos varias vistas, diseño, texto y texto/diseño.

Como cualquier elemento gráfico, un destino también tiene sus propiedades que podemos ver en la paleta de propiedades. Para ver las propiedades de un destino pulsa en él y abre el panel de propiedades si no se abre sólo.

En la siguiente imagen vemos que podemos modificar algunas de las propiedades del destino. Éstas quedarán fijadas en su código xml, al que podemos acceder con la vista testo o con la vista texto/diseño.

Atributos de un Destino

Los atributos del destino con id flow_step_two_dest son:

  • Tipo Fragment,
  • Nombre del fragment
  • Clase java que lo gestionará (FlowStepFragment).
  • Además se le pasa un argumento de tipo Integer con valor por defecto 2. El argumento se llama flowStepNumber.
  • Y también tiene definida una Acción, cuyo id es next_action y su destino es el destino home_dest.

Si seleccionas el Home Destination, verás que su id es home_dest.

Al usar el Navigation Component, vamos a nombrar las pantallas con el sufijo _dest, para marcar que son un destino, independientemente de que sean un fragment o una activity, ahora son destinations.

Vemos que el código de mobile_navigation.xml define todos los destinos, fijarse que son de tipo fragment y sus ids, de momento el home_dest, el flow_step_one_dest y el flow_step_two_dest, que es el que hemos visto.

Analizar el resto de destinos fijándose en sus propiedades o atributos. Fijarse también, al principio del mobile_navitagation.xml que el destino de inicio es home_destination. (app:startDestination=)

El siguiente fragmento de código muestra cómo es el esquema general del navigation graph en su vista de texto. Tenemos los elementos:

  • <navigation> : que es la raiz del gráfo
  • destinations : dentro de <navigation> tendremos varios destinations representados con tags <fragment> o <activity>
  • app:startDestination: es un atributo que especifica el destino que es lanzado cuando el usuario abre por primera vez la aplicación.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools"
            app:startDestination="@+id/home_dest">

  <!-- tags para fragments y activities que forman parte de la navegación>

</navigation>

Volviendo al flow_step_two_dest si mostramos la vista texto o la vista texto/diseño y nos fijamos en su texto en .xml vemos que las propiedades o atributos en el editor se plasman en el xml.

    <fragment
        android:id="@+id/flow_step_two_dest"
        android:name="com.example.basicnavigationbase.FlowStepFragment"
        tools:layout="@layout/flow_step_two_fragment">

        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="2"/>

        <action
            android:id="@+id/next_action"
            app:popUpTo="@id/home_dest"
            app:popUpToInclusive="true"
            app:destination="@id/home_dest"/>
    </fragment>

Los elementos que están definidos dentro del navigation graph, no son el layout del fragment en si, es decir su representación visual, sino que están definiendo qué elementos, fragments o activities, están formando parte de la navegación, son los destinations. La definición gráfica, el layout, de éstos deberá ser definido como siempre con su recurso de tipo layout. Dentro de cada destination podemos definir algunos atributos:

  • android:id: Define el ID para el destino. Este id identfica al destino en el xml y en el código, sea activity o sea fragment, por eso se suele añadir como sufijo _dest, para indicar que es un destino.
  • android:name: declara la clase java que instancia el objeto fragment o activity.
    Nota: fijaros que todos los fragments tienen la misma instancia Java. Esa clase será la responsable de inflar el layout correspondiente al destino al que nos movamos, recibirá un argumento indicando cual es… Lo veremos más adelante….
  • tools:layout: especifica cual es el recurso xml que tiene que ser mostrado en el editor gráfico del navigation graph.
  • <action>: Define las acciones, o rutas hacia los siguientes destinos en la navegación a partir de este destination.
  • <argument>: Define los argumentos que va a recibir el destination, es decir los datos que puede recibir del destination que le llame.
  • <deep-link>: Define un punto de entrada directo a la aplicación. No lo vemos en este tutorial. Ver: CodeLab o Crear Vinculos Directos.

TODOs

En el código hay colocados comentarios que comienzan por //TODO

Estos comentarios marcan el sitio donde hay que ir haciendo las tareas que se proponen.

Deberemos completar los TODO en orden.

Si se clona la versión de Código StepByStep, veréis que hay un Commit para cada TODO, con lo que se puede seguir la progresion de las tareas simplemente haciendo Checkout al commit correspondiente.

Añadir un Destino

TODO STEP 1 Agregando un nuevo destination

Partiendo del código de BasicNavigationBase que os he dejado, vamos a añadir un nuevo destino a nuestro gráfo. La aplicación simplemente muestra la pantalla inicial, no tiene activado ninguno de los enlaces que llevan a los demás destinos. Vamos a ello.

TODO STEP 1

Abrimos el recurso navigation graph, el fichero, mobile_navigation.xml en modo diseño y añadimos el fragment settings_fragment (se me muestran los layouts de activities y fragments que no están ya incorporados al grafo).

En los atributos del recien añadido fragment vemos que su id corresponde con el nombre del fragment. Para seguir la convención de llamar con el sufijo _dest a los destinos, le cambiamos el id, bien en el panel gráfico de atributos o en el texto del xml. Pondremos el id settings_dest.

    <!-- TODO STEP 1 Create a new navigation destination pointing to SettingsFragment -->
    <fragment
        android:id="@+id/settings_dest"
        android:name="com.example.basicnavigationbase.SettingsFragment"
        android:label="settings_fragment"
        tools:layout="@layout/settings_fragment" />

    <!-- TODO END STEP 1 -->

Modificar el grafo para navegar

Activities y Navigation

El componente Navigation sigue las guías marcadas en Principles of Navigation. Estas guías recomiendan que uses actividades como puntos de entrada de tu aplicación. Las actividades también contendrán elementos globales de navegación, como la barra de navegación inferior (Bottom Navigation Bar. Los fragments, por el contrario, pueden verse como los layouts de los destinos específicos.

Layout de la MainActivity. Donde se instancia el my_nav_host_fragment en el centro para renderizar ahí los distintos destinos. Arriba tenemos el Toolbar y abajo el BottomNavigation (una barra de navegación)

Como sabemos, un fragment carga su layout en un <fragment> o en un <framelayout> definido dentro de una activity. Para implementar la navegación con el Navigation component, deberemos crear, en la activity, un widget especial llamado NavHostFragment. Como su nombre indica, se tratará del Fragment que va a hospedar la navegación. En el, se renderizarán todos los fragments de la navegación. Lo que hará el NavHostFragment es intercambiar los fragmnets mediante transactions (ocultas para el programador) con o sin animaciones, conforme esté definida la navegación en el navigation graph.

En nuestro proyecto, la activity principal, carga el layout navigation_activity en el onCreate.

setContentView(R.layout.navigation_activity);

Este layout tiene tres disposiciones, vertical, apaisada y tablet. Si vemos el layout vertical, navigation_activity.xml (h470dp), y vemos como está distribuido su layout en la vista de diseño blueprint (derecha) tenemos que en un linear layout tiene el toolbar en la parte superior, el NavHostFragment en el centro y abajo un menú de navegación.

El widget <>my_nav_host_fragment es el que fragment que contendrá a los demás fragments durante la navegación. En la vista design (izquierda) vemos que nos renderiza el home_fragment.xml porque está definido como fragment inicial en la navegación. El atributo name del my_nav_host_fragment está definido a la clase java que implementa este widget y que se incluyó con las dependencias, androidx.navigation.fragment.NavHostFragment. El atributo navGraph vemos que marca el nombre de nuestro xml de navegación mobile_navigation.xml para que lo analice en su tarea de cambiar los fragments conforme se vaya navegando. El atributo app:defaultNavHost=”true” indica que el botón back del sistema se asocia a este hostFragment para que la backstack navigation la muestre en el fragment.

A continuación vemos el xml completo del layout vertical de la actividad principal. El widget my_nav_host_fragment es un tag <fragment> como vemos.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

    <fragment
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/mobile_navigation" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

Tenemos que darnos cuenta que el fragment my_nav_host_fragment no tiene un xml adicional donde defina su layout. Recodar que cuando diseñamos una activity con fragments, colocamos el tag <fragment> y luego definiamos el xml para el fragment, que se cargaba en el hueco reservado por el tag <fragment>. Aquí no tenemos un layouut my_nav_host_fragment.xml, sin embargo, lo que tenemos es que cada fragment que queramos cargar aquí tendrá su xml layout.

Así tenemos:

  • home_fragment.xml
  • flow_step_one_fragment.xml
  • flow_step_two_fragment.xml
  • etc..

que serán los que se rendericen en el hueco reservado por el tag <fragment> my_nav_host_fragment. Y cuáles son y en qué forma los tiene que cargar viene definido por el navigation graph, y el navigation controller.

Navigation Controller

Cuando el usuario hace algo, como pulsar en un botón, tenemos que lanzar un evento/comando de navegación, para pasar a otra pantalla, por ejemplo. La clase NavController es la que dispara el cambio de fragment en el NavHostFragment.

Para lanzar un comando navigate del controler, primero localizamos el navigation controller y llamamos a su método navigation, pasándole un ID. Este Id puede ser el id de un destination o el id de un action. Estos ids están definidos en el navigation graph en xml.

// Command to navigate to one destination or action
findNavController().navigate( un_ID )

Esto lo hacemos en el fragment activo, es decir, en la pantalla donde está el botón pulsado por el usuario. En nuestro caso esto será la pantalla principal, cuyo layout es el home_fragment.xml y su clase java la HomeFragment. Si abrimos la clase HomeFragment, vemos que teneos dos métodos, onCreateView y onViewCreated y que el STEP 2 de este tutorial está en el onViewCreated. Os dejo el enlace a una cuestión de Stackoverlow donde indican sobre la existencia de ambos métodos y porqué inicializar las vistas que componen el layout de un fragment en onViewCreated en vez de en onCreateView. https://stackoverflow.com/a/38718205

Both onCreateView() and onViewCreated() are called during the CREATED state of a fragment’s life cycle. However, onCreateView() is called first and should only be used to inflate the fragment’s view. onViewCreated() is called second and all logic pertaining to operations on the inflated view should be in this method.

https://stackoverflow.com/a/38718205

NavControler es muy potente, puesto que podemos llamar a métodos como navigate() o popBackStack(), que son traducidos por él, en el código apropiado, basado en el tipo de destino al que estés navegando hacia o desde. Por ejemplo, cuando llamas a navigate() con un destino activity, el NavController llamará a startActivity() en tu nombre, sin que tu tengas que instanciar la llamada a startActivity(). Esto queda oculto para el programador en la propia llamada al método navigate().

Para usar los métodos del objeto >NavControler, primero hay que obtener una instancia del mismo. Podemos acceder a el desde las siguientes funciones, en función de desde dónde estemos realizando la navegación, si desde un fragment, desde una activity o desde una vista:

Tu NavController está asociado con un NavHostFragment. Por eso, independientemente del método que utilices, el objeto, es decir, el fragment, la vista o la activity deben ser un NavHostFragment o heredar de el, sino saltará una IllegalStateException.

Navegar a un Destination con el NavController

Vamos a añadir navegación a nuestra aplicación.

TODO STEP 2

En el botón “Navigate to Destination” del home fragment vamos a asociarle un listener para el click. En el listener implementaremos el callback onClick() y en él usaremos el método navigate(), tras obtener la referencia al NavController. Como estamos en un fragment, utilizaremos la primera función, de las mostradas anteriormente. Como hemos comentado, antes el listener lo instanciaremos en el callback onViewCreated.

Tenemos pues que:

  • Instanciar el boton
  • LLamar al setOnClickListener y añadirle un onClickListener (método dos del artículo Respondiendo al onClick() )
    Nota: al estar en un fragment, el findViewById no está diponible, debemos acceder a través de la view que recibe el callback onViewCreated, que si nos fijamos (debug) podemos ver que se trata del ConstraintLayout definido en el home_fragment.xml. Asi pues usaremos view.findViewById()
  • En el onClick() utilizamos la funcion findNavController apropiada.
    Nota: fijaros que hay que pasarle un fragment. Si ponemos this dentro del onClick ese this hacer referencia al onClickListener, no al fragment, por eso hay definida una variable privada Fragment en la clase.
  • Llamamos al método navigate pasándole el id del destino que queremos, en este caso la primera pantalla, el primer destino, el fragment flow_step_one_dest, fijaros que el id que pasamos es el que tiene dicho fragment en el navigation graph.
  • Ejecutar la aplicación y pulsar el botón, debería navegar a la siguiente pantalla.
        Button navigateButton = (Button) getView().findViewById(R.id.navigate_destination_button);
        navigateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NavHostFragment.findNavController(thisFragment).navigate(R.id.flow_step_one_dest);
            }
        });

El código anterior es el que hemos propuesto, pero podríamos haber utilizado el método createNavigateOnClickListener del componente Navigation (disponible en cualquier sitio) y por tanto haberlo hecho así:

Button navigateButton = (Button) getView().findViewById(R.id.navigate_destination_button);
navigateButton.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.flow_step_one_dest,null));
 

Este método recibe un destination y un Bundle, que en nuestro caso le pasamos un bundle null puesto que no le pasamos ningún argumento. Si quisieramos pasarle argumentos al siguiente fragment llenaríamos el Bundle con los elementos a pasar.


Modificando la Transición entre destinos.

La transición por defecto es simplemente sustituir un fragment por otro. Pero queremos que las transiciones sean más botinas, con efectos …

Para ello se han definido en el código ya unas transiciones. Están definidas como recurso bajo la carpeta anim. Estas son las animaciones que ya están en la aplicación base.

Para realizar la transición con la animación podemos:

  • Definirla en el xml del navigation graph (en la Action)
  • Definirla sobre la marcha incluyendo un conjunto de opciones de navegación con un objeto NavOptions que utiliza un builder para ir construyendo las opciones. Una vez construidas se le pasan al método navigate().

Vamos a utilizar el segundo método para construir una transición a medida, custom transition, que vamos a utilizar en el botón “Navigation To Destination” y puesto que en este botón se utiliza la navegación por destino ( y no por acción), no se toma la definición de las animaciones de transición desde el xml.

Creando una Custom Transition

TODO STEP 3

  • Creamos un objeto de tipo NavOptions.Builder con new NavOptions.Builder() al que iremos añadiendo opciones.
  • Utilizaremos los métodos del builder, .setEnterAnim, .setExitAnim, .setPopEnterAnim, y setPopExitAnim para añadir los ids correspondientes a las animaciones. Para ver las diferencias entre los métodos de setAnim… ver NavOptionsBuilder en A.Dev.
  • Creamos un objeto NavOptions mediante el siguiente código:
final NavOptions options = navOptionsBuilder.build();
  • Finalmente en el boton creamos el listener como en el STEP 2 pero pasándo un tercer parámetro en el médoto navigate().
    Nota: Esto sustituye al STEP2 que habíamos completado antes, con lo que deberíamos comentar lo anterior.
    Para lanzar la navegación partimos del objeto NavHostFragment. Este objeto se instancia por Android cuando infla la navigation_activity.xml puesto que en ella tenemos ese fragment definido en tiempo de diseño, además vemos que su poropiedad app:navGraph está asignada al gráfico de navegación mobile_navigation.xml.
  • Ejecutamos y vemos que la transición, tanto de ida, como al de vuelta (al pulsar el back button) incorporan la animación.

El código completo de este step es:

        //TODO STEP 3 - Set NavOptions
        NavOptions.Builder navOptionsBuilder = new NavOptions.Builder();
        navOptionsBuilder.setEnterAnim(R.anim.slide_in_right);
        navOptionsBuilder.setExitAnim(R.anim.slide_out_left);
        navOptionsBuilder.setPopExitAnim(R.anim.slide_out_right);
        navOptionsBuilder.setPopEnterAnim(R.anim.slide_in_left);

        final NavOptions options = navOptionsBuilder.build();

        navigateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NavHostFragment.findNavController(thisFragment).navigate(R.id.flow_step_one_dest,null,options);
            }
        });
        //TODO END STEP 3

Navegar usando Acciones

El componente navegación también permite usar acciones para navegar entre destinos. Son las flechas que hay entre los destinos en el navigation Graph. Hemos visto que podemos navegar utilizando el id de un destino al invocar al método navigate(), pero si implemento las acciones para navegar, éstas además me permiten:

  • Ver los flujos de navegación que el usuario puede realizar en mi aplicación, en el navigation Graph.
  • Añadir atributos que implementan las animaciones, el paso de argumentos y el comportamiento del Back Button.
  • Poder usar type safe arguments para navegar (comprobados en tiempo de compilación) es decir, pasar datos al destino.

En el código del Navigation Graph, vemos los dos destinos flow_step_one_dest y flow_step_two_dest con sus acciones. El tag <action> está anidado dentro del <fragment> ya que la acción se coloca en el fragment origen. El atributo android:id de la acción indica cual es su identificación para ser invocada desde el código. El atributo app:destination indica cual es el id del destino al que se dirige la acción.

Si nos fijamos en id de la acción en los dos destinos mostrados, vemos que tienen el mismo id, next_action. Esto no es un problema, sino que nos permite un nivel de abstracción, desde el código llamamos siempre a next_action pero la acción sabe a donde ir por su atributo destination. Asi podré estar en un fragment e invocar next_action, que depende donde esté iré a un destino u a otro. Es decir, si estoy en flow_step_one_dest y ejecuto la acción next_action ire al flow_setp_two_dest y desde allí ejecutando otra vez next_action ire a la home.

El atributo add:popUpTo de la acción next_action del flow_step_two_dest, indica que hay que sacar de la backstack todos los elementos que haya entre medias hasta alcanzar el que nos indiquen, en este caso el home_dest. Una vez vaciado el BackStack añade en la cima el nuevo destino que indica el atributo app:destination. Pero en este caso, queremos que no lo añada, porque ese destino es precisamente al que vamos. Por tanto, con el atributo app:popUpToInclusive vacía del backstack el destino mencionado (home_dest). Si no hiciesemos eso, tendríamos que dar dos veces al Back Button para terminar la aplicación al llegar a home desde el flow_step_two_dest. Probar a quitar el atributo app:popUpToInclusive y ver cómo reacciona.

    <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.basicnavigationbase.FlowStepFragment"
        tools:layout="@layout/flow_step_one_fragment">
        <argument
            .../>
        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right"
            app:popEnterAnim="@anim/slide_in_left"/>
    </fragment>

    <fragment
        android:id="@+id/flow_step_two_dest"
        android:name="com.example.basicnavigationbase.FlowStepFragment"
        tools:layout="@layout/flow_step_two_fragment">

        <argument
            .../>

        <action
            android:id="@+id/next_action"
            app:popUpTo="@id/home_dest"
            app:popUpToInclusive="true"
            app:destination="@id/home_dest"/>
    </fragment>

Navegando con acciones

TODO STEP 4

Vamos a dar al botón “Navigate with action” del home fragment el código necesario para navegar a flow_step_one_dest pero esta vez utilizando una accción, la definida en el código anterior, que si nos fijamos ya incluye las animaciones de transición, no hay que crearlas vía el NavOptions.

  • Estos dos pasos ya están en el código, pero se harían así:
    • Abrimos el mobile_navigation.xml graph en vista de diseño seleccionamos el home_dest. Una vez seleccionado añadimos una flecha (acción) desde el home destination (el seleccionado) al flow_step_one_dest.
    • En el cuadro de diálogo de creación, le damos el id (next_action), seleccionamos el destino (flow_step_one_dest) y seleccionamos las animaciones de transición. Recordar que ya están creadas como recursos.
  • Este si hay que hacerlo:
    • Abrimos la clase HomeFragment y añadimos un onClickListener al botón “Navigate with action” en el que llamamos al método navigate() como hicimos en el STEP 2, pero esta vez el id que pasamos es el de la acción, en este caso next_action.

Con las acciones definidas en el .xml el código necesario para implementar la acción de ir de un fragment a otro se reduce sustancialmente.

Lanzar la aplicación y ver cómo se realiza la transición.

        //TODO STEP 4 - OnClickListener to navigate using an action
        Button actionButton = (Button) view.findViewById(R.id.navigate_action_button);
        actionButton.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_action,null));
        //TODO END STEP 4

Uso de los argumentos. Bundles & type safe args

El componente de Navigation tiene un complemento de Gradle llamado Safe Args que genera clases simples de objetos, de manera automática, para una navegación de tipo seguro y acceso a cualquier argumento asociado. Se recomienda el uso de Safe Args para navegar y pasar datos, ya que garantiza la seguridad de tipo. Es decir genera clases por nosotros y comprueba que los tipos de los argumentos que se pasan son correctos en tiempo de ejecución.

Nosotros estamos acostumbrados a pasar argumentos entre destinos mediante Bundles. En el origen de la navegación se construye el Bundle y en el destino se lee. La idea del uso del Bundle es muy fácil, se crea un Bundle y se envía en en el método navigate(). Esto ya lo hicimos en el STEP 3, aunque ahí el Bundle que pasabamos era null. Se crea un objeto Bundle y se transmite al destino mediante navigate(), como se muestra a continuación:

    Bundle bundle = new Bundle();
    bundle.putString("amount", amount);
    Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle);

Ahora en el destino podemos leer el Bundle utilizando el método getArguments() para recuperar el Bundle y acceder a sus contenidos, (recordar parejas clave-valor con tipo).

TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(getArguments().getString("amount"));

El problema puede estar en que los tipos de datos no nos coincidan en tiempo de ejecución. En los ejemplos anteriores es difícil que eso pase porque son muy sencillos pero en otros casos puede darse el caso de cambiar algo en un objeto y no cambiar el método que lo lee, por lo que en tiempo de ejecución dará una excepción. Si pudiésemos comprobar los tipos en tiempo de compilación no tendríamos esos problemas.

Safe args permite que nos quitemos de en medio código como el anterior y lo sustituyamos por algo así:

TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(args.amount);

Debido a su control de tipos en tiempo de compilación, la navegación utilizando las clases generadas por safe args es la manera preferida de navegar, es decir, por acciones y pasando argumentos durante la navegación.

Preparando el proyecto

Tenemos que incluir algunas dependencias en el compilador gradle para que pueda operar con safe args. En el proyecto que estamos siguiendo ya están incorporadas, pero no obstante explicamos como añadirlas a otro proyecto en el que queramos utilizar Navigation con Actions y Arguments.

En el fichero build.gradle de nuestro proyecto, es decir, build.gadle (Project: …. tenemos que añadir una dependencia:

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

Y tenemos que tener Navigation incorporado como previamente se hizo, esto nos añadió en su momento la ext (extension) navigationVersion que vemos a continuación (se encuentra en el mismo fichero, más arriba).

buildscript {

    ext {
        kotlinVersion = '1.3.30'
        appCompatVersion = "1.1.0-alpha04"
        recyclerVersion = "1.1.0-alpha04"
        materialVersion = "1.1.0-alpha05"
        navigationVersion = "2.0.0"
        constraintLayoutVersion = "2.0.0-alpha4"
    }
...

En el fichero build.gradle de la aplicación, es decir, build.gradle (Module: app) tenemos que añadir al principio el plugin safegargs, asi:

apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'

android {
    compileSdkVersion 28
    defaultConfig {
...

Por último tenemos que tener el objeto android.useAndroidX=true en tu archivo gradle.properties, según se indica en Cómo migrar a AndroidX.

Después de habilitar Safe Args, el código generado contendrá las siguientes clases y métodos seguros para cada acción, además de cada destino de envío y recepción.

El código generado automáticamente se ve en el árbol del proyecto, en la vista de Project, bajo la carpeta app\build\generated\source\navigation-args. Son dos tipos de clases generadas automáticamente, las describimos a continuación, A y B.

A) Se crea una clase por cada destino en el que se origina una acción. El nombre de esta clase es el nombre (android:name en xml) del destino de origen, unido a la palabra “Directions”. Por ejemplo, si el destino de origen es un fragmento que se llama HomeFragment, la clase generada se llamaría HomeFragmentDirections.

Esta clase tiene un método para cada acción definida en el destino de origen.

Para cada acción que se usa a fin de pasar el argumento, se crea una clase interna cuyo nombre está basado en la acción. Por ejemplo, si la acción se llama nextAction (o next_action, gradle renombra para asigngar la sintaxis correcta), la clase se llama NextAction. Si tu acción contiene argumentos sin un defaultValue, debes usar la clase de acción asociada para configurar el valor de los argumentos. Es decir sus métodos setter y getter.

B) Se crea una clase para el destino de recepción. El nombre de esta clase es el nombre del destino, unido a la palabra “Args”. Por ejemplo, si el fragmento de destino se llama FlowStepFragment, la clase generada se llama FlowSetFragmentArgs. Usar el método fromBundle() de esta clase para recuperar los argumentos.

Definiendo argumentos en la navegación

Si abrimos el fichero mobile_navigation.xml vemos que ya tenemos incluidos algunos argumentos en nuestra navegación. Los argumentos están definidos a nivel de destino o destination. Son los argumentos que ese destino va a recibir. Es decir, definimos los argumentos que recibimos, no los que enviamos. Ojo, que definir los argumentos es simplemente decir el nombre, tipo y darle un valor por defecto si no nos lo pasan. Veremos cómo podemos pasar el valor del argumento.

Por ejemplo, el código del destino flow_step_one_dest es:

    <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.basicnavigationbase.FlowStepFragment"
        tools:layout="@layout/flow_step_one_fragment">
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>

Vemos como está definidio el argumento “flowStepNumber” de tipo integer y con un valor por defecto de 1.

Si nos fijamos en el código que ya está en la versión inicial de este tutorial, vemos que el fragment FlowStepFragment, es el codigo java asociado a los destinatios flow_step_one_dest y flow_step_two_dest definidos en el navagtion graph. En esta clase, en el método onCreateView podemos acceder a los argumentos. La manera tradicional de obtener el argumento es, sabiendo su nombre y su tipo, codificarlo de esta manera:

int flowStepNumber = getArguments().getInt("flowStepNumber");

Definimos un dato entero, utilizamos getArguments() (que es un método de la clase Fragment que devuelve un Bundle) para obtener el bundle, y de el pedimos un entero con getInt por su nombre. Como hemos dicho necesitamos conocer su tipo. Si este tipo cambiase en origen tendremos que cambiarlo también en destino. Safe Args nos evita al menos esto.

STEP 5

Vamos a sustituir la forma en la que capturamos el argumento.

  • Como estamos en el fragment FlowStepFragment, tenemos a nuestra disposición la clase autogenerada FlowStepFragmentArgs. Llamamos a su método .fromBundle() para obtener el argumento, Al método .fromBundle() hay que pasarle el Bundle, que lo obtenemos con getArguments().
  • Finalmente llamamos al getter de la clase FlowStepFragmentArgs para obtener el argumento.
int flowStepNumber = FlowStepFragmentArgs.fromBundle(getArguments()).getFlowStepNumber();

Hay que comentar la asignación anterior.

        //TODO STEP 5 - Get arguments from autogenerated clases.
        //flowStepNumber = getArguments().getInt("flowStepNumber");
        flowStepNumber = FlowStepFragmentArgs.fromBundle(getArguments()).getFlowStepNumber();
        // TODO END STEP 5

Ya que estamos en la clase FlowStepFragment, sinos fijamos en su código, vemos que independientemente del valor del argumento que llega, siempre muestra/infla el mismo layout destination, en concreto el flow_step_one_fragment. Si no os habías dado cuenta antes, mirar como en el grafo de navegación del flow_step_one_dest pasamos al flow_step_two_dest, pero al ejecutar la aplicación despues del one vuelve a salir el one, y desde este, con la next_action vamos al home. Realmente estamos ejecutando la acción del “two” pero el layout que mostramos para el two es el del “one”. Esto hay que arreglarlo.

STEP 6

Para ello vamos a colocar la lógica necesaria para que se el valor del argumento recibido, flowStepNumber es 1 inflemos el layout flow_step_one_fargment, y si es 2 inflemos el flow_step_two_fragment.

        // TODO STEP 6  - Use type-safe arguments - remove previous line!
        int destLayout = R.id.flow_step_one_dest; //inicializamos a un valor por defecto.
        switch (flowStepNumber) {
            case 1:
                destLayout = R.layout.flow_step_one_fragment;
                break;
            case 2:
                destLayout = R.layout.flow_step_two_fragment;
                break;
        }
//        return inflater.inflate(R.layout.flow_step_one_fragment, container, false);
        return inflater.inflate(destLayout, container, false);
        // TODO END STEP 6

En el STEP 4 utilizábamos el botón “Navigate with Action” para pasar al flow_step_one_dest mediante la acción. Entonces no necesitábamos pasarle ningún argumento y simplemente navegábamos con la acción. El argumento, como no lo pasábamos tomaba el valor por defecto definido en el xml. Hicimos lo siguiente:

        Button actionButton = (Button) view.findViewById(R.id.navigate_action_button);
        actionButton.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_action,null));

Si no queremos definir un valor por defecto, o queremos/podemos pasar un valor diferente tendremos que utilizar una las clases …Direction generadas automáticamente. Para pasar un argumento, éste tiene que estar definido en el .xml, nosotros ahora le pasamos un valor. Hasta ahora no pasábamos un valor, cogíamos el valor por defecto.

STEP 7

Vamos a modificar el STEP 4 (lo comentamos) para pasar un valor de argumento. Usaremos la clase HomeFragmentDirection para ello.

  • Como en el STEP 4, creamos un botón y lo instanciamos, el “Navigate with Action”
  • Le añadimos un onClickListener de la manera tradicional, y en su onClick() callback creamos una variable int flowStepNumber que inicializamos a 1.
  • Ahora obtenemos la action que implementa la clase Direction. Creamos un objeto NextAction de la clase HomeFragmentDirection
  • A ese objeto llamamos a su metodo setFlowStepNumber() (que se crea internamente de manera automática) pasándole nuestra variable flowStepNumber
  • Finalmente llamamos al método navigate() usando el NavControler, como en el STEP 4 pero en este caso en vez de pasar el id del recurso action definido en el .xml pasamos nuestro objeto action, al que le hemos dado su argumento.
        //TODO STEP 7 - Update the OnClickListener to navigate using an action and using  ...Direction clases for arguments
        Button actionButton = (Button) getView().findViewById(R.id.navigate_action_button);
        actionButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int flowStepNumber=1; //
                HomeFragmentDirections.NextAction action = HomeFragmentDirections.nextAction();
                action.setFlowStepNumber(flowStepNumber);
                NavHostFragment.findNavController(thisFragment).navigate(action);
            }
        });
        //TODO END STEP 7

Recordar comentar el STEP 4, ya que este lo sustituye.


Navegando usando Menus, Drawers y Bottom Navigation

Navigation UI

El componente Navigation incluye una clase NavigationUI que tiene métodos estáticos que asocian elementos de menú con destinos de navegación. Si NavigationUI encuentra un menú item con el mismo id que un destination del grafico de navegación, configura el menú item para navegar a ese destino automáticamente.

Usando Navigation UI con un Options Menu

STEP 8

Una de las formas más fáciles de usar el NavigationUI es simplificar la configuración del menú de opciones, Options Menu. En particular, el NavigationUi simplifica el manejo del callback onOptionsItemSelected.

En la MainActivity (java) ya tenemos el código para inflar el overflow_menu en el callback onCreateOptionsMenu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        boolean retValue = super.onCreateOptionsMenu(menu);
        NavigationView navigationView = findViewById(R.id.nav_view);
        // The NavigationView already has these same navigation items, so we only add
        // navigation items to the menu here if there isn't a NavigationView
        if (navigationView == null) {
            getMenuInflater().inflate(R.menu.overflow_menu, menu);
            return true;
        }
        return retValue;
    }
  • Abrir el overflow_menu.xml para incluir el Settings destination
    <!-- TODO Step 8 - Add an item for the settings fragment -->
    <item
        android:id="@+id/settings_dest"
        android:icon="@drawable/ic_settings"
        android:menuCategory="secondary"
        android:title="@string/settings" />
    <!-- TODO END STEP 8 -->

También podemos hacerlo visualmente con el editor gráfico, como se muestra en la imagen.

By default, the back stack will be popped back to the navigation graph’s start destination. Menu items that have android:menuCategory="secondary" will not pop the back stack.

onNavDestinationSelected()
  • Tenemos que gestionar ahora el callback onOptionsItemSelected(MenuItem item).
  • Comentamos el return que ya hay en este callback y llamamos al método onNavDestinationSelected() del objeto NavigationUI.
  • El método onNavDestinationSelected() devuelve true si es capaz de navegar al destino, es decir, si encuentra un destino cuyo id sea igual al del menú item que recibimos.
    Si resulta que el MenuItem que recibimos como parámetro no está destinado para navegar, entonces llamamos al super.onOptionsItemSelected(item). Si no lo encuentra devuelve false. En base a eso sabemos si llamar o no al super.
  • Con esto ya podremos desplegar el menú de opciones y seleccionar Settings, la navegación debería funcionar.
//        return super.onOptionsItemSelected(item);
        // TODO STEP 8 - Have Navigation UI Handle the item selection -
        //  make sure to comment or delete the old return statement above
        // Have the NavigationUI look for an action or destination matching the menu
        // item id and navigate there if found.
        // Otherwise, bubble up to the parent.
        if (NavigationUI.onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_host_fragment)))
            return true;
        else {
            return super.onOptionsItemSelected(item);
        }
        // TODO END STEP 8

Utilizamos la forma Navigation.findNavController(… ) para acceder al NavigationController porque estamos en una Activity, recordar lo visto anteriormente. Recordar que el my_nav_host_fragment es el fragment que está en el layout de la activity, el que recoge todos los destinations.


Usando NavigationUI para configurar la Bottom Navigation

En el layout de la MainActivity, en navigaion_activity.xml (470dp), debajo del fragment my_nav_host_fragment tenemos ya incorporardo un widget para el Bottom Navigation, el BottomNavigationView. Cuyo código vemos a continuación:

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:menu="@menu/bottom_nav_menu" />

Nos fijamos que el atributo app:menu indica el recurso menu cuyo id es bottom_nav_menu. Si busamos ese menú en la carpeta de recursos vemos su código:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@id/home_dest"
        android:icon="@drawable/ic_home"
        android:title="@string/home" />
</menu>

Aunque su apariencia en el menú de diseño nos lo marque arriba como el OptionsMenu, se mostrará allí donde indiquemos, es decir, en el BottomNavigationView que hemos puesto en nuestra main activity. El menú en BottomNavigation sólo tiene un elemento, un item para ir a la pantalla home. Vamos a darle funcionamiento.

SETP 9

ATENCIÓN!! : Para que este punto funcione tal como viene aquí la dependencia del navigation.ui:<version> debe ser la 2.3.5. Para cambiarla ir al menú File->Project Structure->Dependencies seleccionar la dependencia de app y cambiar la versión.

https://stackoverflow.com/a/71369809
  • En la MainActivity implementamos el callback setupBottomNavMenu. (ya está la función puesta con el TODO dentro)
  • Dentro de él, haremos uso del método setupWithNavController(bottomNavigationView: BottomNavigationView, navController: NavController)
  • Para ello instanciamos un objeto BottomNavigationView bottomNav = findView …. para obtener la referencia al BottomNavigationView.
  • Si el objeto no es null, es decir, que tenemos uno, llamamos al método setupWithNavController del NavigationUI pasándole nuestro objeto y el NavControler que recibimos como parámetro en el callback setupBottomNavMenu.

Simplmente con eso, si los items del menú apuntan (se llaman igual) que un destino (en este caso home_dest), la navegación con el menú inferior funcionará ya.

        // TODO STEP 9 - Use NavigationUI to set up Bottom Nav
        BottomNavigationView bottomNav = findViewById(R.id.bottom_nav_view);
        if (null != bottomNav) {
            NavigationUI.setupWithNavController(bottomNav,navController);
        }
        // TODO END STEP 9

Usar NavigationUI para configurar el NavigationDrawer

Por último vamos a utilizar el NavigationUI para configurar la navegacion lateral y el naviaton drawer, incluyeno la gestión del ActionBar y una correcta navegación popup (la que sube a la cima del backstack el destino solicitado). Podremos ver este navigation drawer si tenemos una tablet o ponemos en horizontal el dispositivo activando la vista horizontal, o bien la Split View cuando tenemos el dispositivo en disposición vertical.

Por ello, para la MainActivity tenemos tres layouts en el proyecto como vemos en la imagen. El layout principal, con la pantalla vertical es el que está marcado como (h470dp). Para la tablet se utiliza el layout marcado con (w960dp) y para el modo horizontal en dispositivo móvil tenemos el navigation_activity.xml. Estos dos últimos los vemos en las imágenes.

A la izquierda tenemos el layout horizontal. Como vemos tiene una zona reservada para el NavigationView. En el layout de la izquierda tenemos otro layout para una table en vertical, vemos que el NavigationView si tiene una zona reservada visible en la disposición de la pantalla. Ppdéis ver el código xml de dichos layouts y comprobar la ubicación del NavigationView y cómo está conectado con un nav_drawer_menu, mediante el tag app:menu. En el layout horizontal para dispositivos pequeños, el NavigationView está anidado dentro de un DrawerLayout. Nos fijamos también que el id del NavigationView es nav_view.

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_drawer_menu" />

Si vemos el código del nav_drawer_menu, en él tenemos el item para ir a los destinos Home y al Settings. Vemos que, como en el caso del bottom menu, se muestra en la parte superior en la vista de diseño.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:id="@+id/primary">
        <item
            android:id="@id/home_dest"
            android:icon="@drawable/ic_home"
            android:title="@string/home" />
        <item
            android:id="@+id/settings_dest"
            android:icon="@drawable/ic_settings"
            android:title="@string/settings" />
    </group>
</menu>

STEP 10

Vamos a dar funcionamiento a la navegación lateral.

  • Para ello abrimos el MainActivity.
  • Implementamos el método setupNavigationMenu
  • Dentro instanciamos un objeto para capturar con el findViewById el NavigationView, recordar que su id es nav_view.
  • Al igual que hacíamos con el Bottom Navigation, ahora, si el objeto no es nulo, es decir, si tenemos un Navigation View con ese id en el layout, llamamos al método setupWithNavController del NavigationUI, pasándole nuestro objeto NavigationView y el NavController que recibimos como parámetro.
  • Con esto simplmente ya tendría que funcionar nuestro navigation view.
  • Recordar que los items de los menús deben tener el mismo id que los destinations a los que queramos que nos lleven.
  • Tenemos que desplazar con el dedo (o ratón) el NavigationView puesto que no tenemos todavía el menú hamburguesa superior que lo lance.
    private void setupNavigationMenu(NavController navController) {
        // TODO STEP 10 - Use NavigationUI to set up a Navigation View
        // In split screen mode, you can drag this view out from the left
        // This does NOT modify the actionbar
        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        if (navigationView != null){
            NavigationUI.setupWithNavController(navigationView,navController);
        }
        // TODO END STEP 10
    }

Como hemos comentado, el NavigationView se ve si lo desplazamos con el dedo, pero nos gustaría añadir el menu hamburguesa en el ActionBar.

STEP 11

Para modificar el ActionBar nos hace falta una instancia de AppBarConfiguration. El proposito de AppBarConfiguration es especificar las opciones de configuración que queremos para nuestras toolbars, collapsing toolbars y action bars. Las opciones de configuración incluyen si la barra debe gestionar un drawer layout y que destinos están considerados top-level-destinations. Aquellos destinos que sean considerados top-level-destinations mostrarán un menú de hamburguesa para lanzar el drawer, los que no, mostrarán una flecha atras para volver al destino anterior. Vamos a configurar ahora cuáles de nuestros destinos son top-level-destinations para que se muestren correctamente en el ActionBar.

  • En el onCreate() de la MainActivity, tras la instanciar el navController, comentamos la línea que instancia el AppBarConfiguration y lo sustituimos por lo siguiente.
  • Instanciamos un DrawerLayout para capturar el objeto drawer_layout definido en el .xml.
    Sólo existe en el layout horizontal, pero es el único que va a reaccionar al menú hamburguesa, el de tablet ya muestra el NavigationView, y en el vertical, simplemente no está preparado para ello
  • Si el drawerLayout no es nulo, creamos un objeto appBarConfiguration con new AppBarConfiguration.Builder( …. )
  • El método .Builder(… tiene varias sobrecargas.
    • En una permite añadir, separados por comas los ids de los distintos top-level-destinations.
    • Existe otro .Builder(… que recibe como parametro una colección de ids (sin repetición). Esto lo podemos conseguir con la clase HashSet<> que me devuelve un conjunto sin repetición. Lo asigno a un Set<integer> y luego llamo a su método .add().
  • Utilizamos primero la versión que recibe todos los ids separados por comas. Luego para probar sustituis uno por otro.
    Os dejo abajo el código de las dos formas.
  • Una vez tenemos creado el objeto appBarConfiguration con el set de top-level-destinations, llamo a su método .setDrawerLayout y le paso el drawerLayout que hemos instanciado antes.
  • Y finalment llamamos al método .build() del appBarConfiguration
  • Si resultase que no existiera el drawerLayout, tenemos que llamar al código que hemos comentado anteriormente
        // TODO STEP 11  - Create an AppBarConfiguration with the correct top-level destinations
        //appBarConfiguration =  new AppBarConfiguration.Builder(navController.getGraph()).build();

        // You should also remove the old appBarConfiguration setup above
        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
        if (null != drawerLayout) {
            appBarConfiguration = new AppBarConfiguration.Builder(R.id.home_dest,R.id.settings_dest)
                    .setDrawerLayout(drawerLayout)
                    .build();
        }
        else {
            appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        }

La versión utilizando una Colección de enteros sin repetición es:

        // TODO STEP 11  - Create an AppBarConfiguration with the correct top-level destinations
        //appBarConfiguration =  new AppBarConfiguration.Builder(navController.getGraph()).build();

        // You should also remove the old appBarConfiguration setup above
        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);

        Set<Integer> topLevelDestinations = new HashSet<>();
        topLevelDestinations.add(R.id.home_dest);
        topLevelDestinations.add(R.id.settings_dest);

        if (null != drawerLayout) {
            appBarConfiguration = new AppBarConfiguration.Builder(topLevelDestinations)
                    .setDrawerLayout(drawerLayout)
                    .build();
        }
        else {
            appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        }

Esto no me muestra todavía el menú hamburguesa, pero ya deja preparado el ActionBar para saber si tiene que mostralo o mostrar la flecha.

Sino especificamos los top-level-destinations, sólo el destino inicial es considerado top-level-destination y para todos los demás se mostrará la flecha de back, en vez de el menú hamburguesa.


Ahora vamos a hacer que el ActionBar muestre el menú hamburguesa que desplegará el NavigationView.

STEP 12

Para mostrar el menú hamburguesa tenemos que llamar a NavigationUI.setupActionBarWithNavController().

Si nos fijamos en el código que nos han dado nos han preparado una función setupActionBar(navController, appBarConfiguration); es en ella donde tenemos que llamar a la función indicada, que no solo muestra el icono hamburguesa o la flecha back, sino que muestra un título en el ActionBar en función del top-destination al que podemos vovler.

  • Implementar la llamada y ya debería funcionar el menú hamburguesa.
  • Tenemos también que gestionar el click del botón, para ello implementamos el callback onSupportNavigateUp y dentro llamamos a NavigationUI.navigateUp, usando el mimso AppBarConfiguration.
  • Probar a añadir y quitar el destination settings de los top-level-destinations y ver cómo se comporta diferente.
private void setupActionBar(NavController navController, AppBarConfiguration appBarConfig) {
        // TODO STEP 12 - Have NavigationUI handle what your ActionBar displays
        // This allows NavigationUI to decide what label to show in the action bar
        // By using appBarConfig, it will also determine whether to
        // show the up arrow or drawer menu icon
        NavigationUI.setupActionBarWithNavController(this, navController,appBarConfiguration);
        // TODO END STEP 12
}


    //TODO STEP 12  - Have NavigationUI handle up behavior in the ActionBar
    @Override
    public boolean onSupportNavigateUp() {
        // Allows NavigationUI to support proper up navigation or the drawer layout
        // drawer menu, depending on the situation
        return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.my_nav_host_fragment), appBarConfiguration);
    }
    //END STEP 12

Bien hasta aquí el código de este tutorial, os dejo abajo el enlace para su descarga.

Ejercicio

  1. Añadir más destinos a la navegación, añadir más fragments, también ponerlos en el BottomMenu navigation y en el NavigationView.
  2. Añadir un destino para el carrito de la compra.
  3. Añadir el NavigationView y su NavigationDrawer en la vista vertical (la que no lo incluye).