Toda arquitectura Clean está compuesta por una serie de capas, las cuales separan ciertas partes de nuestra aplicación siguiendo los principios SOLID, para que su desarrollo, escalabilidad y mantenimiento sea fácil.

En este artículo veremos cómo la librería BLoC puede ayudarnos a gestionar el estado de nuestra aplicación, basándose en el manejo de eventos, siguiendo el patrón observer.

¿Qué es BLoC?

BLoC (Business Logic Component) es un componente que sirve de intermediario entre la vista y nuestro modelo de datos. Es similar al concepto de ViewModel (de la arquitectura MVVM), ya que nos facilita gestionar de una forma ordenada los cambios de estado que se producen en el modelo de datos de nuestra aplicación para que, de forma reactiva, la interfaz refleje los cambios necesarios. Se basa en el patrón observer, siguiendo la mecánica de escuchar eventos, y transformarlos en nuevos estados.

Pero ¿qué es un estado?

Si queremos actualizar la interfaz de nuestra aplicación, los widgets nos proporcionan una función setState, que tras ejecutar su contenido, genera la reconstrucción de los mismos, creando de nuevo una configuración que refleje dichos cambios. Por tanto, las variables que tenga nuestro widget, conforman el estado actual.

Por ejemplo, si tenemos un texto que muestra un contador y un botón, el cual incrementa dicho contador, el estado refleja el valor actual de ese número entero. En caso de incrementarlo invocamos a la función setState, que sumará 1 al valor actual, y provocará la actualización de nuestra interfaz:

Text('Pulsaciones: $_counter'),
ElevatedButton(
 onPressed: () => setState(() {
   _counter++;
 }),
 child: const Text("+1"),
)

Con ello, obtendremos el siguiente resultado:

Botón de estado del Flutter bloc.

Más adelante veremos cómo BLoC nos puede ayudar a gestionar este estado, para que nuestro código sea más sencillo de manejar.

Vale, pero ¿dónde encaja BLoC en una arquitectura Clean?

En una arquitectura Clean, no pueden faltar las capas de Presentation, Domain, Data y Entity.

Al utilizar BLoC como gestor de estados, lo incluimos en la capa de presentación, siendo este el puente entre nuestra lógica de negocio y la interfaz de usuario.

BLoC quedaría situado de la siguiente manera:

Diagrama de capas de bloc en una arquitectura.

Cada capa tiene su propia responsabilidad:

  • Presentation: responsable de la interfaz de usuario y sus componentes auxiliares. En nuestro caso, incluimos BLoC.
  • Domain: responsable de la lógica de negocio.
  • Data: encargada de gestionar las distintas fuentes de datos. Puede contar con repositorios, servicios, acceso a datos locales, etc.
  • Entity: contiene las clases relacionadas con el modelo de negocio de nuestra aplicación.

Gestionando la interfaz de usuario con BLoC

Como comentamos anteriormente, BLoC funciona siguiendo el patrón observer, en el cual, cuando el componente recibe un evento, lo procesa y genera un estado:

Estado generado en Bloc.

Volviendo al ejemplo anterior del contador, vamos a gestionar el estado, en el cual tenemos el valor del número entero, y tras un evento de incrementar, sumaremos una unidad y esto generará un nuevo estado con el valor + 1:

Ejemplo del bloc event.

En el código, lo primero que debemos haces es crear una clase CounterState, la cual tendrá una variable con el contador:

class CounterState {
 int counter;

 CounterState({required this.counter});
}

También crearemos una clase evento, llamada IncrementEvent, para emitir un evento con la necesidad de incrementar el contador:

class IncrementEvent {}

Y, por último, crearemos la clase CounterBloc, la cual tiene como genéticos las clases CounterState e IncrementEvent. De esta clase, tenemos que tener en cuenta ciertos aspectos:

  • En el constructor de la clase super, indicaremos el estado inicial, en nuestro caso, una instancia de la clase CounterState, inicializada con el valor 0. Cuando la interfaz necesite nuestro BLoC, este emitirá este estado como inicial, por lo que pintaremos en pantalla el valor 0.
  • Tras el constructor del super, declaramos qué eventos queremos escuchar, para gestionar nuestra lógica de negocio. En cada evento podemos hacer las operaciones que sean necesarias, para posteriormente emitir un nuevo estado, con el valor incrementado.
  • Guardaremos el estado en una variable privada, _counterState, para que a futuros eventos, podamos verificar el valor actual e incrementarlo.
class CounterBloc extends Bloc<IncrementEvent, CounterState> {
 CounterState _counterState = CounterState(counter: 0);

 CounterBloc() : super(CounterState(counter: 0)) {
   on<IncrementEvent>((event, emmit) {
     _counterState = CounterState(counter: _counterState.counter + 1);
     emit(_counterState);
   });
 }
}

Por último, tendremos que colocar el BLoC en la interfaz. Como en Flutter todo es un widget, añadiremos un BlocProvider a nuestro árbol de widgets. Este se encargará de crear nuestro BLoC, para que podamos acceder a él desde los widgets hijos.

Aunque posteriormente veremos que existen varias formas de consumir nuestro BLoC , en este caso utilizaremos un BlocBuilder, que se encargará de reconstruir los widgets hijos en caso de recibir un nuevo estado. Dentro de él, declararemos la función builder, que recibirá el contexto y el estado, del cual obtendremos los datos necesarios para mostrarlos en pantalla:

Center(
 child: BlocProvider(
   create: (context) => CounterBloc(),
   child: BlocBuilder<CounterBloc, CounterState>(
     builder: (context, counterState) {
       return Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           Text('Pulsaciones: ${counterState.counter}'),
           ElevatedButton(
             style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.redAccent)),
             onPressed: () {
               BlocProvider.of<CounterBloc>(context).add(IncrementEvent());
             },
             child: const Text("+1"),
           ),
         ],
       );
     },
   ),
 ),
)

Cómo proveer un BLoC

Como hemos visto en el ejemplo anterior, hay varias formas de inicializar un BLoC. De ello se encargan las clases provider que nos facilita la librería:

  • BlocProvider: crea en nuestro árbol de widgets un BLoC, para que el widget inferior lo tenga disponible cuando lo necesite:
Widget blockProviderExample() {
 return BlocProvider(
   create: (BuildContext context) => CounterBloc(),
   child: Text("This is a BLoC Provider"),
 );
}
  • MultiBlocProvider: crea una lista de BLoCs para que los widgets inferiores puedan combinar varios BLoCs a la vez.
Widget multiBlockProviderExample() {
 return MultiBlocProvider(
   providers: [
     BlocProvider(create: (BuildContext context) => CounterBloc()),
     BlocProvider(create: (BuildContext context) => CounterLessBloc()),
   ],
   child: Text("This is a Multi BLoC Provider"),
 );
}
  • RepositoryProvider: inicializa un repositorio, para que cualquier BlocProvider pueda usarlo.
class Bloc1 extends Bloc<Event1, State1> {
  Repository1 repository1;

  Bloc1({required this.repository1}) : super(State1());
}
class Event1 {}

class State1 {}

Widget repositoryBlockProviderExample() {
 return RepositoryProvider(
   create: (context) => Repository1(),
   child: BlocProvider(
     create: (BuildContext context) => Bloc1(repository1: context.read<Repository1>()),
     child: Text("This is a repository BLoC Provider"),
   ),
 );
}
  • MultiRepositoryProvider: inicializa un listado de repositorios para que los BLoC providers puedan utilizarlos.
class Repository1 {}

class Repository2 {}

class Bloc2 extends Bloc<Event1, State1> {
 Repository1 repository1;
 Repository2 repository2;

 Bloc2({required this.repository1, required this.repository2}) : super(State1());
}

class Event1 {}

class State1 {}

Widget multuRepositoryBlockProviderExample() {
 return MultiRepositoryProvider(
   providers: [
     RepositoryProvider(create: (context) => Repository1()),
     RepositoryProvider(create: (context) => Repository2()),
   ],
   child: BlocProvider(
     create: (BuildContext context) => Bloc2(
       repository1: context.read<Repository1>(),
       repository2: context.read<Repository2>(),
     ),
     child: Text("This is a multirepository BLoC Provider"),
   ),
 );
}

Formas de consumir un BLoC

Una vez tenemos inicializados nuestros BLoCs, solo tendremos que utilizarlos según las necesidades que tengamos:

  • BlocListener: se utiliza para reaccionar a estados que no implican una reconstrucción de la vista. Por ejemplo, mostrar un Toast, una Snackbar o realizar una serie de operaciones en nuestro modelo de datos.
Widget blocListenerExample(){
 return BlocListener<Bloc1,State1>(listener: (context, state){
   if(state.runtimeType == State1){
     //realizar una acción que no esté
     //ligada a nuestro árbol de widgets
     //por ejemplo: mostrar un Toast
     //o un snackbar
   }
 });
}
  • BlocBuilder: para cambiar la configuración de nuestro árbol de widgets e implica una reconstrucción de los mismos. Por ejemplo, si tenemos un listado con una carga de datos mediante un servicio, podemos identificar si está en proceso de carga y mostrar un Loader o presentar los resultados.
Widget blockBuilderExample(){
 return BlocBuilder<Bloc1,State1>(builder: (context, state){
    if(state.runtimeType == State1){
      return const CircularProgressIndicator();
   }else{
      return const Text("Mostrar datos");
   }
 });
}
  • BlocConsumer: se trata de una combinación del uso de BlocListener y BlocBuilder, es decir, podemos recibir eventos y realizar tareas no relacionadas con los widgets, y a su vez, decidir cómo tiene que cambiar la interfaz tras cada evento.
Widget blockConsumerExample() {
  return BlocConsumer<Bloc1, State1>(listener: (context, state) {
    if (state.hashCode == 0) {
      context.read<Bloc1>().add(Event1());
    }
  }, builder: (context, state) {
    if (state.runtimeType == State1) {
      return const CircularProgressIndicator();
    } else {
      return const Text("Mostrar datos");
    }
  });
}

Ejemplo completo usando BLoC en una arquitectura Clean

En este ejemplo, vamos a simular cómo obtenemos una mano de 5 cartas de un mazo. Tomamos como base un servicio REST, que nos genera un mazo de cartas, del cual vamos a poder obtener una mano de 5 cartas.

  • LoadingCardsState: estado de carga para obtener la mano de cartas y la obtención de las primeras 5 cartas. En este caso mostraremos un loader.
  • GetCardsOKState: estado en el cual tenemos las cartas, y podemos mostrarlas al usuario.

Por tanto, tenemos un BLoC para recibir ambos estados. A esta clase la llamaremos CardsBloc.

El resultado, sería el siguiente:

Resultado del flutter bloc

En el siguiente enlace, se puede consultar el código completo, que cuenta con:

  1. Las capas y subcapas:
  • presentation
    • pages: pantallas de nuestra aplicación.
    • router: elemento para gestionar las navegaciones entre pantallas.
    • bloc: BLoCs usados en nuestra aplicación.
  • domain
    • useCase: casos de uso con la lógica de negocio.
    • entities: modelo de la lógica de negocio. En nuestro caso tendremos las entidades de carta y mazo.
  • data
    • repository: repositorios usados.
    • services: llamadas a servicios.
  • di: inyección de dependencias.
  • utils: utilidades como constantes de entorno, loggers y recursos.
  1. Inyección de dependencias usando la librería injector, para que la app sea testable.
  2. API REST DeckOfCardsApi para obtener las cartas.
  3. go_router, como elemento de gestión de navegación simplificada de Navigation 2.0.
  4. logger para mostrar logs por consola.

Conclusiones

Con esto hemos visto los usos más comunes del paquete BLoC, como intermediario entre la capa de interfaz y el modelo de negocio, así como varios ejemplos dependiendo de la casuística que tengamos. BLoC es una pieza que nos es muy familiar si venimos de usar ViewModels o Presenters, y que nos ayuda a crear una máquina de estado reactiva a eventos. Hay otras opciones similares, como Riverpod, Provider o GetIt, pero BLoC es más sencilla, aun requiriendo un poco de boilerplate para montarlo.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.