Flutter Todos Tutorial con “flutter_bloc”

Eduardo CQ
Comunidad Flutter
Published in
12 min readJul 4, 2019

Hola a todos, aquí de nuevo con una traducción más para la comunidad Flutter en español; esta vez para complementar la traducción Flutter patrón BLoC para principiantes como yo. El artículo original de Felix Angelov, lo puedes encontrar aquí.

En el siguiente tutorial, vas a construir una aplicación ‘Todos’ en Flutter usando Libreria Bloc. Para cuando termines, tu aplicación debería verse algo como esto:

Empecemos!!

Vas a iniciar creando un proyecto nuevo de Flutter.

flutter create flutter_todos

Entonces puedes remplazar el contenido de pubspec.yaml con:

y finalmente instalar todas sus dependencias.

flutter packages get

Nota: Estás anulando algunas dependencias porque vas a estar utilizándolas desde los Ejemplos de Arquitectura Flutter de Brian Egan.

Todos Repository

En este tutorial no vas a entrar en los detalles de implementación del TodosRepository porque fue implementado por Brian Egan y está compartido entre todos los Ejemplos de Arquitectura Todo.

En un alto nivel, el TodosRepository voy a exponer un método loadTodos y saveTodos . Eso es prácticamente lo que necesitas saber, así que para el resto del tutorial te enfocarás en el Bloc y capas de Presentación.

Todos Bloc

El TodosBloc será responsable de convertir TodosEvents en TodosStates y administrará la lista de ‘todos’ en tu aplicación.

Modelo

Lo primero que necesitas hacer es definir el modelo Todo. Cada ‘todo’ necesitará tener un id, una tarea, una nota opcional y una bandera opcional completada.

Vamos a crear un directorio models y crear todo.dart.

Nota: Estás usando el paquete Equatable, para que puedas comparar instancias de Todos sin tener que anular manualmente == y hashCode.

Siguiente, necesitas crear el TodosState que recibirá la capa de presentación.

Estados

Vamos a crear blocs/todos/todos_state.dart y definir los diferentes estados que necesitarás manejar.

Los tres estados que necesitas implementar son:

  • TodosLoading — el estado mientras tu aplicación está recuperando ‘todos’ desde el repositorio.
  • TodosLoaded — el estado de tu aplicación después de que ‘todos’ ha sido cargado exitosamente.
  • TodosNotLoaded — el estado de tu aplicación si ‘todos’ no fue cargado exitosamente.

Nota: Estás anotando tu base TodosState con el decorador immutable, entonces eso te indica que todo TodosStates no puede ser cambiado.

Siguiente, vas a implementar los eventos que necesitarás manejar.

Eventos

Los eventos que necesitas manejar en tu TodosBloc son:

  • LoadTodos — le dice al bloc que necesita cargar el ‘todo’ desde el TodosRepository.
  • AddTodo — le dice al bloc que necesita agregar un nuevo ‘todo’ a la lista de 'todos'.
  • UpdateTodo — le dice al bloc que necesita actualizar un ‘todo’ existente.
  • DeleteTodo — le dice al bloc que necesita remover un ‘todo’ existente.
  • ClearCompleted — le dice al bloc que necesita remover todos los ‘todo’ completados.
  • ToggleAll — le dice al bloc que necesita alternar el estado completado de todos los ‘todo’.

Crear blocs/todos/todos_event.dart y vas a implementar los eventos que se describieron anteriormente.

Ahora que tienes TodosStates y TodosEvents implementados, puedes implementar el TodosBloc.

Bloc

Vas a crear blocs/todos/todos_bloc.dart y empieza!! Solo necesitas implementar initialState y mapEventToState.

Tip: verifica la Extención Bloc VS Code , que proporciona herramientas para crear blocs efectivamente para ambas aplicaciones Flutter y AngularDart.

Cuando cedes un estado en los manejadores privados mapEventToState, siempre estarás cediendo un estado nuevo en lugar de mutar el currentState. Esto es porque cada vez que cedes, bloc comparará el currentState con el nextState y desencadenará un cambio de estado (transition) si los dos estados no son iguales. Si solo muta y cede la misma instancia de estado, entonces currentState == nextState evaluaría a verdadero y no se produciría ningún cambio de estado.

El TodosBloc tendrá una dependencia en el TodosRepolsitory de modo que pueda cargar y guardar ‘todos’. Tendrá un estado inicial de TodosLoading y define los manejadores privados para cada uno de los eventos. Cuando el TodosBloc cambia la lista de ‘todos’, llama al método saveTodos en el TodosRepository a fin de mantener todo persistido localmente.

Archivo Barril

Ahora que has terminado con el TodosBloc, puedes crear el archivo barril para exportar todos tus archivos bloc y haz lo conveniente para importarlos más tarde.

Crear blocs/todos/todos.dart y exporta el bloc, los eventos y estados.

Filtrado Todos Bloc

El FilteredTodosBloc será responsable de reaccionar a los cambios de estado en el TodosBloc que acabas de crear y mantendrá el estado de filtered ‘todos’ en tu aplicación.

Modelo

Antes de que empieces definiendo e implementado el TodosStates, necesitarás implementar un modelo VisibilityFilter, que determinará que 'todos' contendrá FilteredTodosState. En este caso, tendrás tres filtros:

  • all — muestra todos ‘todos’(default)
  • active — solo muestra ‘todos’ que no se han completado
  • completed — solo muestra ‘todos’ que se han completado

Puedes crear models/visibility_filter.dart y definir los filtros como u enum:

Estados

Al igual que hiciste con el TodosBloc, necesitarás definir los diferentes estados para el FilteredTodosBloc.

En este caso, solamente hay dos estados:

  • FilteredTodosLoading — el estado mientras que estas recogiendo ‘todos’
  • FilteredTodosLoaded — el estado cuando ya no estás recogiendo ‘todos’

Vamos a crear blocs/filtered_todos/filtered_todos_state.dart e implementar los dos estados.

Nota: El estadoFilteredTodosLoaded contiene la lista de todos tal como el filtro de visibilidad activa.

Eventos

Vas a implementar dos eventos para el FilteredTodosBloc:

  • UpdateFilter — que notifica al bloc que el filtro visibilidad ha cambiado
  • UpdateTodos — que notifica al bloc que la lista de ‘todos’ ha cambiado

Crear blocs/filtered_todos/filtered_todos_event.dart y vas a implementar los dos eventos.

Estás listo para implementar el FilteredTodosBloc a continuación!

Bloc

El FilteredTodosBloc será similar al TodosBloc; sin embargo, en lugar de tener una dependencia en el TodosRepository, tendrá una dependencia del propio TodosBloc. Esto permitirá que FilteredTodosBloc actualice su estado en respuesta a los cambios de estado en el TodosBloc.

Crear blocs/filtered_todos/filtered_todos_bloc.dart y empieza.

Creaste un StreamSubscription para el stream de TodosBloc. Anulaste el método dispose y cancelaste la suscripción, así que puedes limpiar después de que el bloc es desechado.

Archivo barril

Justo como antes, puedes crear el archivo barril para hacerlo más conveniente para importar varias clases ‘todos’ filtradas.

Crear blocs/filtered_todos/filtered_todos.dart y exportar los tres archivos:

Stats Bloc

El StatsBloc será responsable de mantener las estadísticas para el número de ‘todos’ activos y el número de ‘todos’ completados. Similar, al FilteredTodosBloc, tendrá una dependencia en el TodosBloc, de modo que puede reaccionar a cambios en el estadoTodosBloc.

Estado

El StatsBloc tendrá dos estados que pueden estar en:

  • StatsLoading — el estado cuando las estadísticas aún no han sido calculadas.
  • StatsLoaded — el estado cuando las estadísticas han sido calculadas.

Crear blocs/stats/stats_state.dart y vas a implementar el StatsState.

Siguiente, vamos a definir e implementar el StatsEvents.

Eventos

Ahí solo será un solo evento TodosBloc que responderá a: UpdatesStats.
Este evento será enviado cuando el estado TodosBloc cambia de modo que el StatsBloc puede volver a calcular las nuevas estadísticas.

Crear blocs/stats/stats_event.dart y vas a implementarlo.

Ahora estás listo para implementar el StatsBloc que se verá muy similar al FilteredTodosBloc.

Bloc

El StatsBloc tendrá una dependencia en el TodosBloc que le permitirá actualizar su estado en respuesta a cambios de estado en el TodosBloc.

Crear blocs/stats/stats_bloc.dart y vas a empezar.

Eso es todo al respecto!! El StatsBloc recalcula su estado que contiene el número de ‘todos’ activos y el número de ‘todos’ completos en cada cambio de estado del TodosBloc.

Ahora que haz terminado con el StatsBloc , solo tienes un último bloc para implementar: el TabBloc.

Tab Bloc

El TabBloc será responsable por mantener el estado de las pestañas en tu aplicación. Tomará TabEvents como entrada y salida AppTabs.

Modelo / Estado

Necesitas definir un modelo AppTab que también lo usarás para representar el TabState. El modelo AppTab solo será un enum que representa la pestaña activa en tu aplicación. Ya que la aplicación que estás construyendo tiene dos pestañas: todos y stats, necesitarás dos valores:

Crear models/app_tab.dart:

Evento

El TabBloc será responsable para manejar un único TabEvent:

  • UpdateTab — que notifica al bloc que la pestaña activa se ha actualizado.

Crear blocs/tab/tab_event.dart:

Bloc

La implementación de TabBloc será super simple. Como siempre, necesitaras implementar initialState y mapEventToState.

Crear blocs/tab/tab_bloc.dart y vas rápidamente a hacer la implementación.

Te dije que sería simple. Todo lo que TabBloc esta haciendo es configurar el estado inicial para la pestaña ‘todos’ y manejar el evento UpdateTab para ceder una nueva instancia AppBar.

Archivo barril

Por último, crearás otro archivo barril para las exportaciones TabBloc. Crear blocs/tab/tab.dart y exportar los dos archivos:

Bloc Delegate

Antes de pasar a la capa de presentación, implementarás tu propio BlocDelegate que te permitirá manejar todos los cambios de estado y errores en un solo lugar. De verdad es muy útil para cosas como registros de desarrollador o analítica.

Empieza creando blocs/simple_bloc_delegate.dart.

Todo lo que estás haciendo en este caso es imprimiendo todos los cambios de estado (transitions) y errores a la consola solo para que puedas ver que está pasando cuando la aplicación está corriendo en local. Puedes conectar el BlocDelegate a Google Analytics, Sentry, Crashlytics, etc…

Archivo Barril Blocs

Ahora que has implementado todos los blocs, puedes crear el archivo barril.
Crear blocs/blocs.dart y exportar todos los blocs; así que puedes convenientemente importar cualquier código bloc con un solo import.

A continuación, te enfocarás en implementar la pantalla principal en la aplicación Todos.

Pantallas

Pantalla de inicio

El HomeScreen será responsable de crear el Scaffold de la aplicación.
Mantendrá el AppBar, BottomNavigatorBar, tal como los widgets Stats / FilteredTodos (dependendiendo de la pestaña activa).

Vas a crear un directorio nuevo llamado screens donde pondrás todas las pantallas widgets nuevas y entonces crear screens/home_screen.dart.

El HomeScreen será un StatefulWidget porque necesitará crear y hacer dispose al TabBloc, FilteredTodosBloc y StatsBloc.

El HomeScreen crea el TabBloc, FilteredTodosBloc, y StatsBloc como parte de su estado. Usa BlocProvider.of<TodosBloc>(context) para accesar al TodosBloc que estará disponible desde el widget raíz TodosApp (lo veremos más adelante en este tutorial).

Desde que el HomeScreen necesita responder a los cambios en el estado TodosBloc, usas el BlocBuilder para construir el widget correcto basado en el TodosState actual.

El HomeScreen también hace que el TabBloc, FilteredTodosBloc, y StatsBloc estén disponibles para los widgets en su subárbol, mediante el widgetBlocProviderTree del flutter_bloc.

BlocProviderTree(
blocProviders: [
BlocProvider<TabBloc>(bloc: _tabBloc),
BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
BlocProvider<StatsBloc>(bloc: _statsBloc),
],
child: Scaffold(...),
);

es equivalente a escribir

BlocProvider<TabBloc>(
bloc: _tabBloc,
child: BlocProvider<FilteredTodosBloc>(
bloc: _filteredTodosBloc,
child: BlocProvider<StatsBloc>(
bloc: _statsBloc,
child: Scaffold(...),
),
),
);

Puedes ver cómo usando BlocProviderTree ayudas a reducir los niveles de anidación y hacer el código más fácil de leer y mantener.

Siguiente, implementarás el DetailsScreen.

Pantalla Detalles

La DetailsScreen muestra los detalles completos del ‘todo’ seleccionado y permite al usuario ya sea editar o eliminar el ‘todo’.

Crear screens/details_screen.dart y vas a construirlo.

Note: El DetailsScreen requiere un todo’ id, de modo que puede extraer los detalles del todo desde el TodosBloc y para que pueda actualizarse siempre que se hayan cambiado los detalles de una tarea (no se puede cambiar la ID de una tarea).

Las cosas principales a tener en cuenta son que hay un IconButton que envía un evento DeleteTodo , así como un checkbox que envía un evento UpdateTodo.

Hay también otro FlatingActionButton que navega al usuario al AddEditScreen con isEditing establecido a true. Vas a echar un vistazo al AddEditScreen siguiente.

Pantalla Add/Edit

El widgetAddEditScreen permite al usuario, ya sea a crear un nuevo ‘todo’ o actualizar un ‘todo’ existente basado en la bandera isEditing ,eso es pasado vía el constructor.

Crear screens/add_edit_screen.dart y vas a echarle un vistazo a la implementación.

No hay nada específico del bloc en este widget. Simplemente se presenta un formulario y:

  • si isEditing es verdadero el formulario es llenado con los detalles del ‘todo’ existente.
  • de otro modo las entradas están vacías, de modo que el usuario pueda crear un nuevo ‘todo’.

onSave usa una función callback para notificar su padre de la actualización o del recién ‘todo’ creado.

Eso es para las pantallas en tu aplicación, así que antes que lo olvides vas a crear un archivo barril para exportarlas.

Archivo Barril Pantallas

Crear screens/screens.dart y exportar las tres.

Widgets

FilterButton

El widget FilterButton será responsable de proveer al usuario con una lista de opciones de filtro y notificará al FilteredTodosBloc cuando un nuevo filtro es seleccionado.

Vas a crear un directorio nuevo llamado widgets y pon la implementación del FilterButton en widgets/filter_button.dart.

El FilterButton necesita responder a los cambios de estado en el FilteredTodosBloc, así que usa BlocProvider para acceder al FilteredTodosBloc desde el BuildContext. Entonces usa BlocBuilder para volver a renderizar cuando FilteredTodosBloc cambia estado.

El resto de la implementación es Flutter puro y no hay mucho pasando, así que puedes mover el widget ExtraActions.

Extra Actions

Similarmente al FilterButton, el widget ExtraActions es responsable de proveer al usuario una lista de opciones extra: Toggling Todos y Clearing Complete Todos.

Como este widget no se preocupa por los filtros, interactuará con el TodosBloc en lugar de FilteredTodosBloc.

Vas a crear widgets/extra_actions.dart e implementarlo.

Al igual que con el FilterButton, usas BlocProvider para accesar al TodosBloc desde el BuildContext y BlocBuilder para responder a cambios de estado en el TodosBloc.

Basado en la acción seleccionada, el widget envía un evento de TodosBloc a los estados de finalización de ToggleAlltodos’ o ClearCompletedtodos’.

Siguiente, vas a echar un vistazo al widget TabSelector.

Tab Selector

El widget TabSelector es responsable de mostrar las pestañas en el BottomNavigatorBar y manejar la entrada de usuario.

Vas a crear widgets/tab_selector.dart e implementarlo.

Puedes ver que no hay dependencia en blocs en este widget; solo llama a onTabSelected cuando una pestaña es seleccionada y también toma un activeTab como entrada, entonces sabe cuál pestaña está actualmente seleccionada.

Siguiente, echarás un vistazo al widget FilteredTodos.

Filtered Todos

El widget FilteredTodos es responsable de mostrar una lista de ‘todos’ basado en el actual filtro activo.

Crear widgets/filtered_todos.dart y vas a implementarlo.

Al igual que los widgets anteriores que has escrito, el widget FilteredTodos usa BlocProvider para accesar a los blocs (en este caso ambos FilteredTodosBloc y TodosBloc son necesarios).

  • El FilteredTodosBloc es necesario para ayudarte a renderizar el ‘todos basado en el filtro actual.
  • El TodosBloc es necesario para permitirte agregar/eliminar ‘todos’ en respuesta a las interacciones del usuario tales como deslizar sobre un todo individual.

Todo Item

TodoItem es un widget stateless que es reponsable por renderizar un solo ‘todo’ y manejar las interacciones del usuario (taps/swipes).

Crear widgets/todo_item.dart y vas a construirlo.

De nuevo, note que el TodoItem no tiene un bloc específico en el. Simplemente se basa en el ‘todo’ qué pasas vía el constructor y llama la función de callback insertada cuando el usuario interactúa con el ‘todo’.

Siguiente, crearás el DeleteTodoSnackBar.

Delete Todo SnackBar

El DeleteTodoSnackBar es responsable de indicar al usuario que un ‘todofue eliminado y permite al usuario deshacer su acción.

Crear widgets/delete_todo_snack_bar.dart y vas a implementarlo.

Por ahora, estás probablemente notando un patrón: este widget también no tiene un código bloc específico. Simplemente toma un ‘todo’ en orden de parar la tarea y llamar una función callback llamada onUndo si el usuario presiona el botón deshacer.

Casi has terminado; solo dos widgets más!!

Loading Indicator

El widget LoadingIndicator es un widget stateless que es responsable de indicar al usuario que algo esta en progreso.

Crear widgets/loading_indicator.dart y vas a escribirlo.

No hay mucho que discutir aquí; estás solo usando un CircularProgressIndicator envuelto en un widget Center (de nuevo sin código bloc específico).

Por último, necesitas construir el widget Stats.

Stats

El widget Stats es responsable de mostrar al usuario cuántos ‘todosestán activos (en progreso vs completados).

Vas a crear widgets/stats.dart y échale un vistazo a la implementación.

Estás accediendo al StatsBloc usando BlocProvider y usando BlocBuilder para reconstruir en respuesta a cambios de estado en el estado StatsBloc.

Poniéndolo todo junto

Vas a crear main.dart y el widgetTodosApp. Necesitas crear una función main y ejecutar el TodosApp.

Nota: estas configurando el delegado de BlocSupervisor en el SimpleBlocDelegate que creaste antes, para que puedas enganchar todas las transiciones y errores.

Siguiente, vas a implementar el widget TodosApp.

El TodosApp es un widget stateless que crea un TodosBloc y ponlo a disposición a través de toda la aplicación usando el widget BlocProvider de flutter_bloc.

El TodosApp tiene dos rutas:

  • Home — que pasa un HomeScreen
  • AddTodo — que pasa un AddEditScreen con isEditing establecido en falso

El main.dart completo debería parecerse a esto:

¡Eso es todo al respecto! Ahora has implementado una aplicación de ‘todos’ en flutter usando los paquetes bloc y flutter_bloc y has separado con éxito nuestra capa de presentación de la lógica de negocios.

La fuente completa de este ejemplo se puede encontrar aquí.

Si disfrutaste de este ejercicio tanto como yo, puedes apoyarme en el repositorio o 👏 esta historia.

--

--