Índice de contenido
- ¿Qué es un estado?
- Manejadores de estado
- Árbol de widgets y manejadores de estado
- Provider
- Provider: Instalar y primeros pasos
- Provider: Configuración inicial
- Crear Provider
- Registrar el provider
- Escuchar los cambios
- Ciclos infinitos empleando los Provider u otros manejadores de estado en Flutter - CUIDADO
- Una anécdota con Providers en Flutter
- La estructura del Provider
- El peligro de los bucles infinitos
- El error “invisible”
- ¿Por qué sucede esto?
Un problema que tenemos actualmente en la aplicación es que, al momento de crear o editar sitios y volver al listado, no vemos reflejado estos cambios sobre dicho listado; para verlos, debemos de reiniciar la aplicación o simplemente cerrar la aplicación y volver a abrirla; esto es un problema que puedes escalar en otros muchos escenarios, que se construyen grandes aplicaciones con muchas ventanas y cuya solución es la misma que si se tratase de una pequeña aplicación como es este el caso.
¿Qué es un estado?
Antes de sumergirse en la creación de aplicaciones y la gestión de estados en Flutter, es necesario comprender qué es realmente un estado y cómo afecta a nuestra aplicación.
El estado de una aplicación no es más que una condición; una condición de cómo se encuentra la aplicación en un momento determinado.
Por ejemplo, su aplicación muestra dos variables, a y b, con valores 5 y 8 respectivamente; si hay una interacción del usuario que puede ser cualquier cosa, una conexión a internet, el usuario da un click a un botón... En definitiva, los valores de las variables a y b cambian a otro valor, digamos 4 y 5, ese sería un estado diferente de la aplicación, Estado B.

Manejadores de estado
Los manejadores de estado no son más que un mecanismo con el cual podemos realizar la gestión del estado de la aplicación.
Manejar el estado de la aplicación permite realizar un seguimiento de cada cambio dentro de la misma y da la posibilidad de notificar los cambios a lo largo de toda la aplicación; en definitiva, permite:
- Responder a las interacciones del usuario.
- Notificar los cambios en el estado en las diferentes pantallas de la aplicación
Árbol de widgets y manejadores de estado
El árbol de widget en Flutter, que no es más que una representación a nivel de nodos de cómo se relacionan los widgets en Flutter y que presentamos en los primeros capítulos de este libro; es importante tenerlo en mente para conocer las ventajas que nos ofrecen los manejadores de estado en Flutter.
Para una aplicación sencilla como es la que tenemos y podemos simplificar como:

La numeración de los nodos es simplemente una identificación y no tiene que ver con alguna prioridad.
Debemos de notificar al nodo 3, que es del formulario para crear/editar sitios, al nodo 2, que, aunque el nodo dos es padre del nodo 3, y se podría implementar un mecanismo de comunicación entre ambos como se realizó anteriormente cuando se trabajó en la selección de una imagen provista por el usuario:
_ImageField(
key: Key(_keyImageField),
onSelectedImage: _selectedImage,
imageDefault: _routeImage,De pasar mediante una función o similar, el sitio editado o creado; pero, qué pasa si la comunicación no solamente es a el nodo directo, si no a otros nodos que forman parte de otras ramas o están más arriba (o abajo) del nodo que se encarga de aplicar dicho cambio.
Realizar comunicaciones directas entre widgets es algo que se tiene que llevar con cuidado; la solución anterior es ideal para una funcionalidad que sabemos será limitada (seleccionar una image, colocar en un contenedor y notificar a su padre sobre la ruta del archivo) pero, cuando se trata del manejo de elementos globales como lo son, por ejemplo, un contador, datos del usuario autenticado, un listado (de sitios como es nuestro caso) y en pocas palabras, cualquier temática que se maneje de manera global a la aplicación, como publicaciones, imágenes, videos, registros etc; lo mejor que se puede usar son manejadores de estados; esto se debe a que, puede que la aplicación no funcione de la manera esperada cuando tenga nuevas pantallas, comportamientos entre otros tipos de cambio; aparte de que:
- Las pantallas que forman tu aplicación tendrían implementaciones adicionales que no forman parte de la funcionalidad que permite dicha ventana, si no para comunicar a otras pantallas (padres o hijas).
- Esto da como resultado de que, la aplicación quedaría menos mantenible y escalable.
- Más tiempo para desarrollar la aplicación del necesario.
Y en definitiva, espero que estés convencido de que es necesario implementar en muchos casos algún manejador de estado como Provider.
Provider
El manejador de estado Provider para Flutter es muy sencillo de usar, configurar y extender; por lo tanto, resulta más fácil de entender cómo funcionan otros manejadores de estado como BLoC o Redux partiendo de Provider; con esto, no queremos decir que no puedes usar Provider en una aplicación real; cada manejador de estado tiene sus puntos fuertes y débiles; por lo tanto, resulta necesario conocer varios de ellos para saber cuál funcionará mejor en nuestra aplicación.
Su web oficial es:
https://pub.dev/packages/provider
Provider: Instalar y primeros pasos
Para instalar Provider, lo hacemos mediante el siguiente paquete:
pubspec.yaml
dependencies:
flutter:
sdk: flutter
// ***
provider:Provider: Configuración inicial
El uso de Provider es muy sencillo, basta con indicar tantas clases como entidades quieras manejar; por ejemplo:
Si tuvieras un usuario autenticado, cuyos datos pueden cambiar a lo largo de la aplicación y que dichos datos se utilizan en todas o en varias pantallas, puedes crear una clase provider para manejar los datos del usuario autenticado.
Si tuvieras un listado de elementos (de sitios, por ejemplo) tendríamos una clase provider para manejar dicho listado.
Si tuvieras un contador u otro objeto global, otra clase provider para el mismo.
Por lo tanto, en una aplicación existente, si tienes otra entidad en la cual manejar su estado; creas otra clase provider y la registras en la aplicación.
Crear Provider
En el caso de la aplicación que estamos llevando a cabo, basta con definir un solo provider, para la entidad que estamos manejando, que serían sitios, en otras palabras, un listado de sitios.
Creamos dicha clase:
lib\providers\PlacesProvider.dart
import 'package:flutter/material.dart';
import 'package:place/helpers/db_helper.dart';
import 'package:place/models/place.dart';
class PlacesProvider with ChangeNotifier {
List<Place> _places = [];
List<Place> get places {
return [..._places];
}
Future<void> getAndPlaces() async {
_places = await DBHelper.places();
notifyListeners();
}
}Explicación del código anterior
La clase extiende de ChangeNotifier la cual nos permite usar el método de notifyListeners() que forma parte de la API de Flutter y según la documentación oficial:
Llame a este método cada vez que cambie el objeto, para notificar a los clientes que el objeto puede haber cambiado...
Con este método, cada vez que hagamos unos cambios sobre la entidad que queremos manejar el estado (sitios en este caso), notificamos a los escuchadores, que obviamente serían nuestras pantallas.
En la clase anterior, definimos un método get para la entidad a manejar el estado, es importante notar que, la entidad es privada, por lo tanto, solamente puede ser accedida desde la clase, y para hacerla pública se hace de manera controlada, tanto:
- Para obtener los datos (que se hace mediante la creación de otra propiedad con el mismo nombre, pero publica, en la cual se clonan los datos de la misma mediante el operador de propagación -esto es importante ya que, con esto se evita que el widget cambie los datos de la entidad a manejar el estado de manera directa-).
- Como para inicializar la entidad a manejar el estado (_places), con esto, ganamos un punto extra, y es que, separamos parte de la logica que implementamos para manejar los datos, de la presentación de los datos; al hacer un cambio a nivel de _places, notificamos los cambios mediante la función de notifyListeners() para notificar a los oyentes (la pantalla de listado de sitios en este caso).
Registrar el provider
Todos los providers que queremos usar, los tenemos que registrar en el punto más alto de la aplicación en los cuales los vayamos a usar; en la mayoría de los casos, lo puedes registrar en el widget padre de la aplicación, que sería en el MyApp, con esto, aseguras que lo puedes usar en toda la aplicación, aunque es importante notar que, lo puedes colocar en widgets internos a esta, por ejemplo una pantalla y por lo tanto, solamente pudieras emplear los providers en el widget que lo registres (por ejemplo la pantalla) y sus hijos.
Finalmente, si quieres usar un único provider, puedes usar la siguiente implementación:
lib\screens\add_place_screen.dart
import 'package:flutter/material.dart';
import 'package:place/providers/PlacesProvider.dart';
import 'package:provider/provider.dart';
void main() => runApp(ChangeNotifierProvider.value(
value: PlacesProvider(),
child: const MyApp(),
));
// ***O si quieres usar varios:
import 'package:flutter/material.dart';
import 'package:place/providers/PlacesProvider.dart';
import 'package:provider/provider.dart';
void main() => runApp(MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => PlacesProvider(),
),
],
child: const MyApp(),
));
// ***Para la aplicación que estamos llevando a cabo, puedes usar la que prefieras.
Escuchar los cambios
Ya cumplimos con dos pasos fundamentales:
- Crear el provider.
- Registrar el provider.
El siguiente paso, es usarlo, para ello, existen múltiples variantes como usar métodos para inicializar crear, editar, actualizar, y consumir; vamos a empezar con el método que nos permite inicializar el listado de sitios:
lib\screens\index_place_screen.dart
// ***
@override
void initState() {
_init();
super.initState();
}
_init() async {
//places = await DBHelper.places();
await Provider.of<PlacesProvider>(context, listen: false).getAndPlaces();
setState(() {});
}
// ***Con la clase anterior, estamos indicando que vamos a usar un provider:
Provider.of
De tipo PlacesProvider (el único que tenemos):
Provider.of<PlacesProvider>Y el listen, se tiene que usar cuando definimos el provider y no queremos reconstruir un widget; si no colocas el listen en falso, tendrías una excepción como la siguiente:
Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.E indicamos cual es recurso que queremos obtener, ya sea funciones o propiedades (públicas):
Provider.of<PlacesProvider>(context, listen: false).getAndPlaces();Ahora, con el listado inicializado, lo siguiente que vamos a hacer, es construir el listado, para eso, consumimos el listado clonado de _places:
lib\screens\index_place_screen.dart
// ***
@override
Widget build(BuildContext context) {
places = Provider.of<PlacesProvider>(context).places;
// ***En este caso, no usamos el listen en false, ya que, nos interesa obtener la respuesta para redibujar el widget.
En definitiva:
- Si la respuesta del provider no va a ser usada directamente para redibujar un widget, se coloca el listen en false.
- Si la respuesta del provider va a ser usada para redibujar un widget, no se usa la opción de listen.
Ciclos infinitos empleando los Provider u otros manejadores de estado en Flutter - CUIDADO
Una anécdota con Providers en Flutter
En este apartado, quería contarte una anécdota muy interesante (si se le puede llamar así) que tuve con los Providers. Es lo que estoy utilizando, por ejemplo, para definir el tema de la aplicación. Fíjate en cómo tengo estructurado el código:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appModel = Provider.of<AppModel>(context, listen: false);
_initData(context);
return MaterialApp(
// ...
theme: appTheme.currentTheme,
// ...
);
}
_initData(BuildContext context) async {
final appTheme = Provider.of<ThemeSwitch>(context, listen: false);
}La estructura del Provider
Esto es para quienes ya manejan un poco de Flutter, pero igual lo explico rápido. Arriba tengo el Main con la inicialización de varias cosas. Tengo un par de Providers: uno para el tema y otro para el modelo de datos (AppModel). Aunque hay muchas formas de definirlos, yo los coloco anidados en la parte más alta de la aplicación, justo en el runApp, para que estén disponibles globalmente.
El Provider del tema se encarga de gestionar el modo oscuro y el modo claro. En la práctica, esto se controla mediante un switcher en el drawer. Lo ubicamos en el nivel más alto para que, al seleccionar un tema, el cambio se propague en cascada hacia abajo por toda la aplicación, aplicando los colores de mi marca (como ese moradito que siempre uso).
El peligro de los bucles infinitos
Al ser un Provider, el sistema se encarga de escuchar los cambios:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appModel = Provider.of<AppModel>(context, listen: false);
_initData(context);
// …Aquí es donde surgió el problema. Quería hacer una especie de "sincronización". Como la aplicación de la Academia también está disponible en web, si cambio al modo claro y tengo el mismo usuario autenticado, la app móvil debería reaccionar y sincronizarse según lo que esté establecido en la base de datos.
El error “invisible”
Si quito el condicional y recargo la aplicación, sucede algo curioso. Al principio parece que funciona, pero si analizas bien, te das cuenta de que algo anda mal. La comunicación con el servidor no es full-duplex; entonces, ¿cómo es que se sincroniza "automáticamente"?
Para descubrir qué pasaba, coloqué un print en la función:
Dart
_initData(BuildContext context) async {
print("test");
final appTheme = Provider.of<ThemeSwitch>(context, listen: false);
}Al revisar la consola, vi que el mensaje "test" aparecía sin parar. Se estaba llamando en ciclos, enviando múltiples peticiones por segundo a nuestro servidor. Si se conectan mil usuarios así, colapsas el servidor en un punto y, además, le consumes todo el internet al cliente.
¿Por qué sucede esto?
Incluso teniendo el parámetro listen: false, el problema es que todo ocurre dentro del mismo widget (MyApp). Al actualizar el Provider dentro del _initData, se notifica un cambio que obliga a redibujar la aplicación, lo que vuelve a ejecutar el build, que a su vez vuelve a llamar al _initData, creando un bucle infinito.
class MyApp extends StatelessWidget {
_initData(BuildContext context) async {
final appTheme = Provider.of<ThemeSwitch>(context, listen: false);
// Esto dispara el redibujado de la app
appTheme.darkTheme = userPreference.themeDarkMode = darkMode;
}Este es un error muy común y difícil de detectar a simple vista. Tu aplicación puede crashear sin sentido, ponerse lenta o colapsar al intentar cargar miles de registros o imágenes en bucle.
El mensaje que te quiero dar es este: ten mucho cuidado con la lógica de inicialización. Asegúrate siempre de que no existan estos bucles infinitos, porque pueden arruinar por completo la experiencia del usuario y la estabilidad de tu servidor.