Flutter Todos Tutorial con “flutter_bloc”
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 convertirTodosEvents
enTodosStates
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 elTodosRepository
.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 elTodosBloc
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 completadocompleted
— 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 cambiadoUpdateTodos
— 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, alFilteredTodosBloc
, tendrá una dependencia en elTodosBloc
, 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 salidaAppTabs
.
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 elScaffold
de la aplicación.
Mantendrá elAppBar
,BottomNavigatorBar
, tal como los widgetsStats
/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 widget
AddEditScreen
permite al usuario, ya sea a crear un nuevo ‘todo’ o actualizar un ‘todo’ existente basado en la banderaisEditing
,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á alFilteredTodosBloc
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 widgetExtraActions
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 ToggleAll
‘todos’ o ClearCompleted
‘todos’.
Siguiente, vas a echar un vistazo al widget TabSelector
.
Tab Selector
El widget
TabSelector
es responsable de mostrar las pestañas en elBottomNavigatorBar
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 ‘todo’ fue 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 ‘todos’ está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 unHomeScreen
AddTodo
— que pasa unAddEditScreen
conisEditing
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.