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:

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:

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:

Widget blockProviderExample() {
 return BlocProvider(
   create: (BuildContext context) => CounterBloc(),
   child: Text("This is a BLoC Provider"),
 );
}
Widget multiBlockProviderExample() {
 return MultiBlocProvider(
   providers: [
     BlocProvider(create: (BuildContext context) => CounterBloc()),
     BlocProvider(create: (BuildContext context) => CounterLessBloc()),
   ],
   child: Text("This is a Multi BLoC Provider"),
 );
}
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"),
   ),
 );
}
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:

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
   }
 });
}
Widget blockBuilderExample(){
 return BlocBuilder<Bloc1,State1>(builder: (context, state){
    if(state.runtimeType == State1){
      return const CircularProgressIndicator();
   }else{
      return const Text("Mostrar datos");
   }
 });
}
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.

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:
  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.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.