Aprendiendo del nuevo Navigator 2.0 y del sistema de Routing

Argel Bejarano
Comunidad Flutter
Published in
10 min readJan 5, 2021

Este articulo es una traducción el original lo puedes encontrar aquí, en el cual John Ryan nos ayuda un poco a entender estos conceptos. Como un comentario personal, yo creo que este es un feature que realmente necesitamos, sin embargo, también pienso que le falta madurar ya que para poder hacer una implementación básica de estas APIs se requiere de un basto conocimiento, algo que es bueno por que puede incentivar a aprender, pero también puede hacer que alguien que este entrando el mundo de Flutter sienta demasiado complicada una simple navegación entre pantallas.

Dicho esto comencemos con él articulo, recuerden, como todo el tiempo se mantendrá la traducción sin modificación para respetar el trabajo realizado por el escritor original.

Este articulo explica cómo las nuevas API de Navigator y Router funcionan. Si has estado siguiendo el design docs de Flutter, habrás visto que estos nuevo features conocidos como Navigator 2.0 and Router. Exploraremos como estas APIs habilitan un control mas especifico sobre nuestras pantallas de nuestra app y cómo puedes usarlos para parsear rutas.

Estas nuevas APIs no son breaking changes, simplemente agregaron una nueva API declarativa. Antes del Navigator 2.0, era difícil de hacer push or pop a multiples paginas, o quitar paginas debajo de la actual. Como sea, si estas contento con la forma de trabajar con el Navigator actualmente, puedes continuar usando de la misma forma (imperativo).

El Router provee la habilidad de manejar las rutas de la plataforma utilizada y mostrar la pagina correcta. En este artículo, el Router esta configurado para parsear la URL de tu browser y mostrar la pagina correcta.

Este articulo ayuda a elegir cuál patrón deNavigator funciona mejor para tu app y explica cómo utilizar el Navigator 2.0 para parsear las URLs y tomare total control sobre el stack de páginas que están activas. El ejemplo en este artículo muestra como construir una app que maneja las rutas entrantes de la plataforma y administra las páginas de tu app. El siguiente GIF muestra un ejemplo de la app en acción:

Navigator 1.0

Si estas utilizando Flutter, probablemente estas Navigator y te son familiares los siguientes conceptos:

  • Navigator — un widget que administra el stack de objetos Route.
  • Route — un objeto administrado por un Navigator que representa una pantalla, normalmente implementada por una clase como MaterialPageRoute.

Antes de Navigator 2.0, Routes eran pushed y popped dentro del stack del Navigator ya sea con named routes o anonymous routes. La siguiente sección es una pequeño resumen de estos dos acercamientos.

Anonymous routes

La mayoría de las app mobiles muestran las pantallas una encima de la otra, como un stack. En Flutter eso es fácil de lograr utilizando el Navigator.

MaterialApp and CupertinoApp hacen uso de Navigator under the hood. puedes acceder al navigator usando Navigator.of() o mostrar una nueva pantalla con Navigator.push() , y regresar a la pantalla siguiente con Navigator.pop() :

Cuando es llamado push() , el widget DetailScreen es puesto encima de HomeScreen de la siguiente manera:

La pantalla anterior ( HomeScreen ) aun es parte de nuestro árbol del widgets, entonces cualquier otro objeto en nuestro State asociado con él se mantendrá mientras DetailsScreen este visible.

Named routes

Flutter también soporta rutas nombradas, las cuales son definidas en el parámetro de routes en MaterialApp o CupertinoApp:

Estas rutas deben estar predefinidas, aunque también puedes pasar argumentos a una ruta nombrada, no puedes parsear argumentos desde la misma ruta. Por ejemplo, si la app esta ejecutando en web, no puedes parsear un ID desde una ruta así /details/:id .

Advanced named routes with onGenerateRoute

Una forma más flexible de manejar rutas nombrada usando onGenerateRoute . Esta API permite la habilidad de manejar todos tus

A more flexible way to handle named routes is by using onGenerateRoute. This API gives you the ability to handle all paths:

Aquí esta un ejemplo completo:

Aquí, settings es una instancia de RouteSettings . Los campos name y arguments son los valores que son proveídos cuando llamamos Navigator.pushNamed , o cuando initialRoute esta listo.

Navigator 2.0

El API de Navigator 2.0 agrega una nueva clase al framework como una manera de hacer que las pantallas de las apps una funciona de nuestro estado en la app y proveer la habilidad de parsear rutas desde la plataforma utilizada (como URLs web). Aquí podrás ver una vision general de que es lo nuevo:

  • Page — un objeto inmutable usado para iniciar el stack de historial del navigator.
  • Router — configura la lista de las páginas a ser mostradas por el Navigator. Normalmente esta es una lista de páginas que cambia dependiendo de la plataforma, o sobre el estado cambiante del app.
  • RouteInformationParser, el cual toma laRouteInformation de RouteInformationProvide y lo parsea dentro de una data-type definido por el usuario.
  • RouterDelegate — define el comportamiento especifico de la app de cómo el Router aprende acerca de los cambios en el estado de la app y cómo responde a ellos. Su trabajo es escuchar RouteInformationParser y el estado de la app y construir él Navigator con la lista actual de Páginas .
  • BackButtonDispatcher — notifica al Router cuando el botón de back es presionado.

El siguiente diagrama muestra como RouterDelegate interactúa con Router , RouterInformationParser , y el estado de tu app:

Aquí esta un ejemplo de como estas piezas interactúan entre ellas:

  1. Cuando la plataforma emite una nueva ruta (por ejemplo, "books/2"), el RouteInformationParser lo convierte dentro data typeT abstracto el cual definirás en tu app (por ejemplo, una clase llamada BooksRoutePath ).
  2. El método setNewRoutePath del RouteDelegate es llamado con este data type, y debe actualizar el estado de la aplicación para reflejar el cambio (por ejemplo, al seleccionar el selectdBookId ) y llamar notifyListeners .
  3. Cuando notifyListeners es llamado, le pide al Router reconstruya el RouterDelegate (usando su método build() )
  4. RouterDelegate.build() regresa un nuevo Navigator, en la cual sus páginas reflejas el cambio en el estado de la app (por ejemplo, selectedBookId).

Ejercicio de Navigator 2.0

En esta sección te guiara a través de un ejercicio utilizando el API de Navigator 2.0. Terminaremos con un app que puede permanecer sincronizada con le URL en tu browser, y también gestionar el botón regreso desde el browser, como lo muestra el siguiente GIF:

Para seguirlo, cambia al canal master, crea un nuevo proyecto con soporte Web en Flutter, y reemplaza el contenido de lib/main.dart con lo siguiente:

Pages

El Navigator tiene un nuevo argumento en su constructor llamado pages . Si la lista de objeto Page cambia, Navigator actualiza e stack de rutas para comparar. Para ver cómo es que esto funciona, construiremos un app que muestra una lista de libros.

En _BooksAppState , mantén dos partes del estado, una lista de libros y el libro seleccionado:

Entonces en _BooksAppState , regresa un Navigator con una lista de objetos Page :

Ya que la app tiene dos pantallas, una lista de libros y una pantalla mostrando los detalles, agreguemos una segunda (detalles) pantalla si el libro es seleccionado (usando collection if )

Te podrás dar cuenta que la key para las páginas esta definida por el valor del objeto book . Esto le dice al Navigator que este objeto MaterialPage es diferente de otro cuando el objetoBook es diferente. Sin una key única, el framework no podría determinar cuándo mostrar una animación de transición entre diferentes Pages .

Nota: Si lo prefieres, también puedes extender de Page para personalizar el comportamiento. Por ejemplo, esta página agrega una animación de transición personalizada:

Finalmente, es una error proveer un argumento a pages sin también agregar uno al callback de onPopPage . Esta funciones llamada cuando sea que Navigator.pop() es llamado. Debería ser utilizado para actualizar el estado (esto determina la lista de páginas), y debe ser llamado didPop sobre la ruta para determinar si el pop fue exitoso.

Es importante verificar cuando sea que didPop falle antes de actualizar el estado de la app.

Utilizando setState notifica al framework para llamar el método build() , el cual regresa una lista con una pagina cuando _selectedBook es null.

Aquí esta el ejemplo completo:

Tal como está, esta app solo nos habilita como definir una stack de páginas de una forma declarativa. No seremos capaces de manejar el botón de regreso de la plataforma, y la URL del browser no cambia mientras navegamos.

Router

Hasta el momento, el app puede mostrar diferentes páginas, pero no puede manejar rutas de la plataforma base, por ejemplo, si el usuario actualiza la URL de su browser.

Esta sección muestra cómo implementar el RouteInformationParser , RouteDelegate , y cómo actualizar nuestro estado en el app. Una vez implementado, el app se mantendrá sincronizada con la URL del browser.

Data types

El RouteInformationParser parsea la ruta de información dentro de un data type definida por el usuario, así que definiremos esto primero:

En esta app, todas nuestras rutas pueden ser representadas utilizando una sola clase. En lugar de esto, podrías elegir utilizar diferentes clases que implementen una super clase, o administren la información de la ruta de otra manera.

RouterDelegate

Siguiente, agregar la clase que herede de RouterDelegate :

El tipo genérico definido sobre RouterDelegate es BookRoutePath , la cual contiene todo lo que el estado necesita para decidir cuál pagina mostrar.

Necesitaremos mover lógica de _BookAppState a BookRouterDelegate , y crear un GlobalKey . En este ejemplo, el estado de la app esta almacenado directamente en el RouterDelegate , pero también podría estar separada dentro de otra clase.

Para poder mostrar el path correcto en una URL, necesitamos regresar un BookRoutePath en base a nuestra app actual:

Siguiente, el método build en un RouteDelegate necesita regresar una Navigator :

La callback onPopPage ahora utiliza notifyListeners en lugar de setState , ya que esta clase ahora es ChangeNotifier , no un widget. cuando el RouterDelegate notifica a sus listeners, el widget de Router es igualmente notificado que la currentConfiguration de RouterDelegate ha cambiado y que su método build tiene que ser llamado de nuevo para construir un nuevo Navigator .

El método _handleBookTapped también necesita usar notifyListener en lugar de setState :

Cuando una nueva ruta ha sido llamada a la aplicación, Router llama a setNewRoutePath , el cual da la oportunidad a nuestra app de ser actualizada en base a los cambios en la ruta.

RouteInformationParser

El RouteInformationParser proveed un hook para parsear las rutas entrantes ( RouteInformation ) y convertirlo en una tipo definido por el usuario ( BookRoutePath ). Usando la clase Uri para que se encargue del parsing:

Esta implementación es especifica para esta app, no una solución general para el parsing. Más de esto después.

Para usar estas nuevas clases, necesitaremos usar el nuevo constructorMaterialApp.router y pasarlo en nuestra implementación personalizada.

Aquí esta un ejemplo completo:

Ejecutando este ejemplo en Chrome ahora nos muestra las rutas tal cual están siendo navegadas, y navegar a la página correcta cuando la URL es editada manualmente.

TransitionDelegate

Puedes proveer una implementación personalizada de TransitionDelegate que personalize como nuestras rutas aparecen (o son eliminadas de) la pantalla cuando la lista de las páginas cambia. Si necesitas personalizarlas, lee lo siguiente, pero si eres feliz con el comportamiento por defecto te puedes saltar esta sección.

Agrega un TransitionDelegate al Navigator que define el comportamiento deseado:

Por ejemplo, la siguiente implementación deshabilita todas las animaciones de transición:

Esta implementación personalizada sobre escribe resolve() , el cual esta a cargo de marcar como las rutas son ya sea pushed, popped, added, completer o removed:

  • markForPush — Muestra la ruta con una transición animada
  • markForAdd —Muestra la ruta sin una transición animación
  • markForPop — Quita la ruta con una transición animada y completa con un resultado. "Completando" en este contexto significa que el objeto result se pasa al callback onPopPage sobre el AppRouterDelegate
  • markForComplete — Quita la ruta sin una transición y completa con un result
  • markForRemove — Quita la ruta sin transición de animada y sin completar

Esta clase solo afecta el API declarativa, por loa cuál el botón de regreso aun esta mostrando una transición animada.

Como este ejemplo trabaja: Este ejemplo revisa ambos la nueva ruta y las rutas que existen en pantalla. Va a través de todos los objetos en newPageRouteHistory y las marca para ser agregadas sin una transición animada usando markForAdd. Después, hace un bucle en todos los valores del mapa locationToExitingPageRoute. Si este encuentra una ruta marcada como isWaitingForExitingDecision, entonces llama al markForRemove para indicar que la ruta debe ser eliminada sin una transición y sin completar.

Aquí tienes el ejemplo completo (Gist).

Nested routers

Este demo mas grande muestra como agregar un Router dentro de otro Router . Muchas apps requieren rutas para destinos en una BottomAppBar . y rutas para una stack de vistas encima de ella, el cual requiere dos Navigators. Para lograr esto, el app utiliza un objeto de aplicación de estado para almacenar el estado de navegación especifico del app (El index del menu seleccionado y el objeto del Book seleccionado). Este ejemplo también muestra cómo configurar cuál Router maneja el botón para ir atrás.

Ejemplo de router anidado(Gist)

Qué sigue?

Este articulo explora cómo usar estas APIs para una app especifica, pero puede también ser utilizado para hacer un API package de alto nivel. Esperamos que te nos unas a explorar que API de alto nivel crear para nuestros usuarios encima de estos features .

Eso es todo por hoy, solo para que tomen en cuenta, este articulo fue originalmente publicado el día 29 de Septiembre del 2020 y dependiendo de cuando lo estes leyendo algunas cosas pueden variar.

Te dejo mi Twitter handle por si gustas seguirme donde siempre estoy publicando información acerca de Flutter y cualquier tema relacionado con el.

https://twitter.com/ArkangelB

Te dejo también mi canal de YouTube (Community Brain) con contenido acerca de Flutter:

--

--

Argel Bejarano
Comunidad Flutter

Flutter & Dart GDE | Speaker and Editor from Comunidad Flutter | Founder @EsFlutter