Flutter, Bloc y Firestore Stream, ¡la combinación perfecta, si se usa de la manera correcta!

Arnaudelub
5 min readMar 4, 2021

Version disponible en Français: Flutter, Bloc and Firestore Stream, la combinaison parfaite, si utilisée de la bonne manière!

This story is also available in english: Flutter, Bloc and Firestore Stream, the perfect match, if used the right way!

Mi primera experiencia con Flutter

En 2018, el día después de ser contratado en una empresa como desarrollador de Angular, finalmente me dijeron que mi trabajo no sería desarrollar en Angular, sino desarrollar aplicaciones usando Flutter. Estaba como, ¡WTF! ¡Ese no era el trato y nunca había escuchado nada sobre Flutter antes !. Pero luego pensé, oye, ¿por qué no? Me gusta programar y aprender cosas nuevas.

Entonces me dieron 10 días de entrenamiento, solo, empezando con el cookebook de flutter (https://flutter.dev/docs/cookbook).

Después de 2 días practicando, honestamente ya estaba enamorado de Flutter, un código para Android e iOS, y escritorio y web en el futuro (hoy, ya lo tenemos, Flutter 2.0.0… OLEE !!!!), eso es simplemente asombroso.

Entonces, al final de mi capacitación, listo para empezar, me preguntaron si AWS sería una buena opción para el backend, y sí, claro que lo es, pero les dije, "¿también han estado considerando Firebase?" Quiero decir, es una plataforma de Google, por lo que la compatibilidad con Flutter es probablemente del 100%. Y adivinen qué, después de eso, finalmente empiece a desarrollar mi primera aplicación con Flutter y Firebase. ¡Guai!

El camino de dev a prod con mi primera aplicación, no fue un camino fácil, he estado luchando mucho con algunos grandes errores que casi le cuestan a la compañía algo de dinero con la facturación de Google.

Así que voy a intentar enumerar algunos de ellos, así que espero que no te suceda.

Empezamos

De acuerdo, lo primero es lo primero, configura tu proyecto de Firebase (hay mucha documentación para eso) y, al principio, mantenga el plan Spark, que es completamente gratuito, no se solicita tarjeta de crédito y casi tienes todas las funciones de Firebase disponibles. Pero si tu o tu empresa desean cambiar al plan Blaze desde el principio, lo siguiente que debes hacer es ir a https://console.cloud.google.com/billing/, seleccionar su proyecto y en el menú de la izquierda haga clic en Presupuesto y alerta, siga las instrucciones y establezca el presupuesto mensual en 1€ y las alertas. Si no lo haces y si estas usando StreamBuilder con algun snapshots de Firebase de manera incorrecta, es posible que termines teniendo un mal día. ¿Por qué ?, te preguntarás, bueno, aquí viene:

El StreamBuilder

Así que está creando esta buena aplicación, y te hanpedido crear esta pantalla donde el contenido debe actualizarse automáticamente cuando los datos cambian en la base de datos, y adivina qué, las Stream de Firebase y StreamBuilders son perfectos para este trabajo y si eres algo nuevo en Flutter, probablemente codifiques algo así dentro de un StatefullWidget:

StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('myCollection').doc(item.id).snapshots(),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if(snapshot.hasData) {
//Parsing your data, using setState(), etc...
} else {
//CircularProgressIndicator, handling error, etc...
}
});

En esas pocas líneas de código, hay varias cosas mal. El primero es llamar cada vez que FirebaseFirestore.instance, FirebaseFirestore debe instanciarse como Singleton y para eso puede usar el plugin injectable (https://pub.dev/packages/injectable) y usar algo como

getIt<FirebaseFirestore>()

para llamarlo.

A continuación, el DocumentSnapshot TIENE que ser declarado en su initState si está utilizando un StatefullWidget. Si no lo haces, cada vez que se reconstruya su widget, el Stream se iniciará nuevamente hasta que se elimine el widget. Imagina que es un QuerySnapshot con cientos de documentos, alcanzará la lectura gratuita de 50,000 documentos en poco tiempo e incluso puede ser peor si está usando setState dentro del constructor, ya que causará un bucle infinito de reconstrucción y no lo deseas.

Ahora, si realmente quieres hacerlo correctamente, escribe tu repositorio usando injectable nuevamente, usa el decorador @lazySingleton, aprovecha la inyección de dependencia para inyectar la instancia FirebaseFirestore.instance a tu clase, será mucho más limpio. Aquí hay un ejemplo de cómo lo hago normalmente (más o menos):

Normalmente, el error también debe manejarse dentro del repositorio, estoy usando Either del paquete Dartz (https://pub.dev/packages/dartz) para eso, pero es para otra historia.

Usando Stream con Bloc

Así que esta es una de las primeras cosas más importantes que he aprendido de mi primer proyecto, pero el otro gran problema que puedes tener es que los widgets se están reconstruyendo en todas partes y está totalmente fuera de tu control, por lo que tu aplicación se está volviendo lenta, demasiado pesada. y la razón es que usualmente usamos StatefullWidget en todas partes al principio. Lo que pasa con StatefullWidget es que si el estado cambia, o sea cuando se usa setState(), se está reconstruyendo todo el método build(). Entonces, si deseas un control más fino sobre la reconstrucción, puedes dividir tanto como puedas tu widget con otros widgets, o puedes usar Bloc con el paquete flutter_bloc (https://pub.dev/packages/flutter_bloc) por ejemplo, y empezar a usar StatelessWidgets.

Y con eso, viene escuchar Stream usando el patrón de bloque.

En realidad, todo esto proviene de una pregunta que encontré en Slack el otro día y lo he estado ayudando a implementar esto. Creo que es una pregunta legítima y la respuesta no te viene directamente a la mente.

Para poder hacer esto, necesitaras tener dos eventos, uno para llamar a tu Stream del repositorio y escucharlo y otro para llamar cuando se reciben nuevos datos.

Para mantener el mismo ejemplo que antes, digamos que queremos escuchar este método getLogs(), tendras que crear esos dos eventos:

const factory LogEvent.watchLogsAsked() = WatchLogsAsked;
const factory LogEvent.logsReceived(List<Log> data) = LogsReceived;

ahora, en nuestro archivo de Bloc, podemos implementar la lógica para obtener el State que queremos, pero primero debemos suscribirnos a los eventos del Stream y Stream.listen devuelve un StreamSubscription. Entonces tenemos:

StreamSubscription<List<Log>> _logsStreamSubscription;

Ahora podemos seguir y “yield” nuestro State en función de los eventos:

yield* event.map(
watchLogsAsked: (WatchLogsAsked _) async* {
yield state.copyWith(logsAreLoading: true);
await _logStreamSubscription?.cancel();
_logsStreamSubscription = _repository.getLogs().listen((data) => add(LogEvent.logsReceived(data)));
},
logsReceived: (LogsReceived data) async* {
//Do your stuff here and yield your state
}
);

Y eso es básicamente todo, ahora puedes llamar a BlocBuilder en tu StatelessWidget y hacer todo lo que quiera con sus datos, sabiendo que será la única parte que se reconstruirá cuando lleguen nuevos datos;

Y no te olvides cancelar tu suscripción también, simplemente “override” el método Bloc close() y cancela la suscripción allí.

Aquí esta un ejemplo completo de como lo uso:

Puedes ver que estoy usando freezed para la generación de código y para el manejo de errores, si quieres saber más sobre eso, hay muchos tutoriales por aquí que explicarán cómo funciona mejor que yo.

Bueno, esa fue toda una historia, y es solo mi punto de vista, todos sabemos que hay muchos y no pretendo que este sea el mejor. Esta es solo una posibilidad y hay muchas más, y esa es una de las mejores cosas que tenemos, siempre podemos aprender cosas nuevas, nunca nos aburrimos.

No dudes en hacerme saber lo que piensas, si te gusta o no y por qué.

Happy coding a todos!

--

--