Aprendiendo del nuevo Navigator 2.0 y del sistema de Routing
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 unNavigator
que representa una pantalla, normalmente implementada por una clase comoMaterialPageRoute
.
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
deRouteInformationProvide
y lo parsea dentro de una data-type definido por el usuario.RouterDelegate
— define el comportamiento especifico de la app de cómo elRouter
aprende acerca de los cambios en el estado de la app y cómo responde a ellos. Su trabajo es escucharRouteInformationParser
y el estado de la app y construir élNavigator
con la lista actual dePáginas
.BackButtonDispatcher
— notifica alRouter
cuando el botón deback
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:
- 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 llamadaBooksRoutePath
). - El método
setNewRoutePath
delRouteDelegate
es llamado con este data type, y debe actualizar el estado de la aplicación para reflejar el cambio (por ejemplo, al seleccionar elselectdBookId
) y llamarnotifyListeners
. - Cuando
notifyListeners
es llamado, le pide alRouter
reconstruya elRouterDelegate
(usando su métodobuild()
) RouterDelegate.build()
regresa un nuevoNavigator
, 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 animadamarkForAdd
—Muestra la ruta sin una transición animaciónmarkForPop
— Quita la ruta con una transición animada y completa con un resultado. "Completando" en este contexto significa que el objetoresult
se pasa al callbackonPopPage
sobre elAppRouterDelegate
markForComplete
— Quita la ruta sin una transición y completa con unresult
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.
Te dejo también mi canal de YouTube (Community Brain) con contenido acerca de Flutter: