Photo by Michael Niessl on Unsplash

Riverpod: Reescribiendo Provider

Entendiendo Riverpod sin morir en el intento

Marcos Sevilla
Published in
10 min readFeb 17, 2021

--

Creo que no es una exageración decir que la mayoría del contenido sobre Flutter es sobre manejo de estado, o state management si sos bilingüe.

Siendo un framework reactivo para el lado del cliente, Flutter tiene integrado desde su nacimiento el concepto de estado. Pero si haces un proyecto complejo, setState no va a resolverte, por eso hay manejadores de estado.

La herramienta que ayuda al manejo de estado más popular de Flutter es Provider. No lo digo yo, lo dice la ciencia:

Estadísticas de Provider en pub.dev

Seguido de paquetes como Bloc que incluso depende del mismo Provider, así de útil es.

No voy a hablar mucho de Provider porque yo ya hice una aclaración que considero importante en mi artículo sobre StateNotifier, es una lectura obligatoria ya que te aclara bastante lo que es Provider (ni explico porque estoy seguro que ya saben qué es) y es uno de los paquetes que incorpora Riverpod.

Pero… ¿qué es Riverpod y por qué me gusta tanto?

Causa

En el artículo de StateNotifier les expliqué que Provider era una herramienta que nos permite usar fácilmente InheritedWidget, que es la clase en Flutter que dispone sus propiedades a sus hijos en el árbol de widgets.

Provider en sí tiene muchos problemas por esta relación con InheritedWidget.

Primero, no nos permite crear dos Providers del mismo tipo. Si tenemos una clase de la que queremos crear dos objetos por aparte para que manejen lógica similar en distintos módulos del app, se nos hace imposible porque un InheritedWidget se encuentra en el árbol buscando un tipo de dato -o clase- en específico. Esto también provoca que no se encuentren Providers y nos deja errores en tiempo de ejecución que se podrían evitar en compilación. Strike uno.

Luego, combinar Providers puede ser engorroso, más complejo de lo que debería y lanza errores inesperados por la misma manipulación inusual de InheritedWidget. La forma común de lograr que un Provider esté al tanto de los cambios en otro para reaccionar en su lógica es usando un ProxyProvider o ChangeNotifierProxyProvider y aún así estos no son las mejores alternativas en sincronización o sintaxis (se hace muy anidado), vienen siendo una inyección de dependencias y no exactamente una escucha a eventos. Strike dos.

Por último, al depender de InheritedWidget, depende de Flutter, lo cual no es lo mejor si te gusta encapsular tu lógica de negocio en un paquete por aparte y mantener una separación de concernimientos con tu lógica independiente del SDK de la interfaz gráfica. Strike tres... bueno, no necesariamente pero, ¿ya ven que hay muchas oportunidades de mejora?

Efecto

Todos estos problemas hicieron que el mismo creador de Provider -me pongo de pie y aplaudo- Rémi Rousselet, tuviera la iniciativa de hacer una reingeniería de InheritedWidget desde cero, para de esa manera crear Riverpod.

Riverpod en este momento se encuentra en una versión estable para uso en producción, pero como tal es un proyecto experimental que no es un reemplazo oficial de Provider, aún.

En este link pueden ver el estado de Riverpod según Rémi.

En simples palabras, Riverpod no depende de Flutter así que no depende de InheritedWidget, nos permite combinar Providers para que escuchen sus cambios y reaccionen a ellos, y nos garantiza código confiable desde la etapa de compilación.

Sí, resuelve los problemas de Provider, y muy bien si me lo preguntan.

Vamos a verlo en acción y sus equivalencias con respecto a Provider.

Cuál paquete ocupo

Si haces una búsqueda en pub.dev, vas a encontrar 3 versiones de Riverpod, dejame te explico qué hace cada una.

  • riverpod - Contiene el núcleo de Riverpod, la funcionalidad básica para utilizarlo sin depender de Flutter. Es el que debes usar si estás creando un paquete para una capa de lógica de negocio aparte de la interfaz gráfica.
  • flutter_riverpod - Contiene el núcleo de Riverpod, sumado a todas las clases y widgets que se utilizan para enlazar la lógica con la interfaz, depende de Flutter. Es el que debes ocupar si tu proyecto ya necesita de Flutter y no estás separando la lógica en otro paquete fuera del proyecto. La más común por cómo organiza sus proyectos la mayoría.
  • hooks_riverpod - Contiene el núcleo de Riverpod y adicionalmente la funcionalidad necesaria para ser usado en conjunto con flutter_hooks, el cual debe ser añadido también como dependencia. Es el que debes de usar si tu proyecto ocupa hooks para simplificar tu código en la interfaz gráfica y Riverpod para acceder al estado. No voy a profundizar mucho en este paquete en este artículo.

Y si no quedó claro, esta imagen está en la documentación, para hacerlo más gráfico:

Cómo saber cuál Riverpod usar

Migrando de Provider a Riverpod

De MultiProvider a ProviderScope

Inicialmente cuando queríamos insertar un Provider en el árbol de widgets, hacíamos algo así…

Acá podíamos tener una lista de nuestros Providers y dependía del MultiProvider, un sólo Provider o ChangeNotifierProvider. Esta forma nos daba un ámbito o alcance que determinaba en qué parte de nuestro árbol de widgets íbamos a poder ocupar nuestra clase de lógica.

En Riverpod, nuestros Providers pueden ser vistos como variables globales que podemos consumir igualmente en un alcance global llamado ProviderScope y este alcance puede sobreescribirse a como sea necesario en el árbol.

Nueva forma de crear de Providers

Una vez declarado nuestro ProviderScope, podemos utilizar bajo él cualquier Provider. Ahora veamos cómo hacemos un Provider.

En el caso de Provider creamos una clase que herede de ChangeNotifier, StateNotifier o una clase propia de repositorio o servicio. Riverpod trae por defecto StateNotifier, así que es mejor implementar esta alternativa, de igual manera hay más tipos de Providers de los que vamos a hablar en un instante.

Este NamesNotifier podemos utilizarlo tanto en Provider como Riverpod porque los componentes de lógica no se ven afectados por el paquete, que ya se encarga de incorporar en el árbol de widgets este componente para su uso.

En Provider necesitamos incluir los paquetes state_notifier y flutter_state_notifier, en Riverpod no los necesitamos ya que flutter_riverpod contiene los ConsumerWidget (equivalente al widget de Consumer en Provider pero se utiliza más parecido a StatelessWidget).

💡 Aclaro rápidamente que el núcleo de los paquetes está en y state_notifier, ya los otros paquetes que mencioné que se llaman igual con la diferencia del prefijo flutter contienen los widgets para comunicar la lógica de estos paquetes con la interfaz.

Para crear un Provider de cualquier tipo en Riverpod, se hace de la siguiente manera…

Y para crear un Provider de StateNotifier en Riverpod, es…

De una vez declarado, sin importar en qué archivo se encuentre, el Provider está en el ProviderScope y está disponible donde se necesite mediante ProviderReference.

Quiero que analicen un poco la sintaxis.

  1. Las declaramos como una variable final.
  2. El Provider se crea con la clase Provider, la cual contiene una función callback por la cual devolvemos la clase. Ojo: esta función nos va a servir de mucho más adelante.
  3. El callback nos devuelve una variable de tipo ProviderReference, que llamamos ref, para nuestro uso en la creación del Provider.

La variable ref es lo que vendría a ser un BuildContext para Providers en Riverpod. Con ella podemos acceder a otros Providers en el árbol y lo bueno es que podemos acceder a ella en la creación de cualquier Provider.

¿Ven por qué era bueno separarlo de Flutter? Ya no necesitamos BuildContext.

Uso de Providers en la interfaz gráfica

En Provider utilizamos el BuildContext del método build() de cualquier widget para acceder a nuestro Provider ya que es un InheritedWidget.

Podemos hacerlo de dos maneras…

  • Utilizamos un ConsumerWidget.
  • Utilizamos un Consumer widget.

Sí, hay un ConsumerWidget que es muy similar a un StatelessWidget, pero agrega un ScopedReader como parámetro al método build(). Este nuevo parámetro nos permite acceder a cualquier Provider que nosotros creemos y estar escuchando (u observando) los cambios que pueda tener.

Igualmente, podemos usar un StatelessWidget en conjunto con el widget Consumer que funciona de la misma manera que en Provider, sólo que nos da nuestro ScopedReader en lugar de el tipo que nosotros le definamos, ya que pueden haber múltiples Providers con el mismo tipo y eso ya no nos garantiza que obtengamos el valor que deseamos al enviar el tipo.

💡 En esta sección de Qué recomienda Marcos…

Yo uso ConsumerWidget ya que no me gusta anidar más widgets con el Consumer, el código me parece mucho más legible y es más sencillo buscar dónde se ocupan tus Providers teniendo clasificados tus widgets en StatelessWidget, StatefulWidget y ConsumerWidget.

Combinando Providers

Una de las soluciones que trae Riverpod a los problemas de Provider es una comunicación más sencilla entre los Providers. Esto lo logramos gracias al ProviderReference.

La creación de un Provider nos brinda por defecto su posible combinación con otro que esté en la referencia. Es así como podemos inyectar dependencias de Providers y establecer comportamientos basados en esos cambios.

Tomando en cuenta que tenemos un Provider con funcionalidad de autenticación, al que llamamos authProvider, utilizamos su método login() que nos devuelve un token de acceso. Teniendo el token, podemos actualizar el estado de nuestro tokenProvider, que nos guarda el valor para usarlo en otras consultas.

¿Qué? ¿Qué es eso de StateProvider y FutureProvider? Qué bueno que preguntan…

Todo es un Provider

Como pueden ver, hay muchos tipos de Providers, y es una palabra que les va a salir hasta en la sopa si trabajan con Riverpod.

Al igual que Flutter te dice que consideres todo un widget en el framework, en Riverpod les sugiero que consideren todo un Provider. Un Provider puede ser una función (en sí se construyen con funciones), una variable de estado, una variable global (literalmente) y así sucesivamente.

Tipos de Provider

Les voy a presentar los tipos de Providers más comunes y utilizados hasta el momento.

  • StateProvider — Expone un valor que puede ser modificado desde fuera. A como lo vimos en el ejemplo de antes, accedemos al estado por medio de la variable state y podemos cambiarlo. Muy similar a ValueNotifier.
  • FutureProvider — Construye un valor asíncrono o AsyncValue y tiene el funcionamiento de un Future común y corriente. Es útil para cargar datos de algún servicio cuyo método para obtener estos datos sea asíncrono, como lo vimos en el ejemplo con un valor que viene de una llamada asíncrona y luego la guarda en un StateProvider.
  • StreamProvider — Crea y expone el último valor en un Stream. Ideal para comunicaciones en tiempo real con Firebase o algún otro API cuyos eventos se puedan interpretar en Stream.
  • StateNotifierProvider — Crea un StateNotifier y expone su estado. El más usado para los componentes de lógica, permite acceder fácilmente a state, que usualmente es una propiedad privada que se modifica mediante métodos públicos.
  • ScopedProvider — Define un Provider<T> (de cualquier tipo) que se comporta de manera diferente en alguna parte del proyecto. Se utiliza en conjunto con la propiedad overrides de ProviderScope y se define con este último el alcance donde este ScopedProvider se va a modificar.
  • ProviderFamily — Este tipo de Provider es un modificador, es decir, todos los anteriores pueden utilizarlo. En sí, define un parámetro adicional que se ve involucrado en la creación de nuestro Provider, como un String.
  • AutoDisposeProvider — Otro modificador que nos permite hacer dispose() a nuestros Providers automáticamente, permitiéndonos borrar el estado al ser destruidos los widgets donde se utilizan.

Mejores prácticas

Cómo estructurar Providers

Riverpod tiene la peculiaridad que, a como vimos anteriormente, cubre muchos casos de uso con distintos tipos de Providers, desde estado de app a estado efímero.

El problema es que no hay una guía sobre cómo estructurar Providers, por eso les recomiendo siempre separar su lógica de una buena manera.

Te lo resumo en mis reglas personales:

  • Separar carpetas del proyecto por funcionalidades (features) y núcleo (core).
  • En el núcleo usar una carpeta /providers y separar en un archivo cada Provider que vayas a utilizar en múltiples partes de tu app. Por ejemplo, yo siempre ocupo package_info con un Provider en el núcleo de mis proyectos.
  • En una funcionalidad en específica procuro tener una carpeta de /logic donde tengo todos mis Providers.
  • No tengo más de un componente de lógica (StateNotifier) por funcionalidad.
  • En la lógica separo mi estado, componente de lógica y Provider en archivos por aparte.

Con las reglas que les comenté antes, nos queda una estructura de carpetas así…

Estructura de Providers por funcionalidad

Algunas cosas a tomar en consideración

Estuve pensando un buen rato si hay alguna desventaja o algo que decir como advertencia sobre Riverpod. Siendo sincero y objetivo, no encuentro muchas cosas y las que encuentro no son críticas.

Riverpod ya está en una fase estable, como pudieron ver. Pueden implementarlo desde ya en sus proyectos y me atrevo a decir que puede ser una mejor opción que Provider. Es un paquete que te puede llevar tu tiempo dominar, pero una vez que lo haces, se vuelve una herramienta muy capaz.

Igualmente aclaro que no es un reemplazo de Provider aún, ya que Provider aún resuelve muchas necesidades. Para mí, Riverpod va a ser el nuevo estándar una vez haya más desarrolladores dominándolo y se entiendan los problemas que resuelve, ya que muchos aún no han encontrado escenarios donde Provider muestre sus fallas.

Lo de siempre…

Si aprendiste algo nuevo y te fue de utilidad, podés compartir este artículo para ayudar a otro/a desarrollador(a) a seguir mejorando su productividad y calidad al escribir aplicaciones con Flutter.

También hay una versión de este mismo artículo en inglés publicado en dev.to. De nada. 🇺🇸

Además, si te gustó este contenido, podés encontrar aún más y seguir en contacto conmigo en mis redes sociales:

  • dev.to — donde publico versiones en inglés de mis artículos.
  • GitHub — donde están mis repositorios de código por si te gustan los ejemplos.
  • LinkedIn — donde conecto profesionalmente.
  • Medium — donde estás leyendo este artículo.
  • Twitter — donde expreso mis ideas cortas y comparto mi contenido.
  • Twitch — donde hago directos informales de los que saco clips con información puntual.
  • YouTube — donde publico los clips que salen de mis directos.

Originally published at http://github.com.

--

--