Patrón Bloc en flutter

Noé Montes AlvarezFlutter

Hola de nuevo. Hoy vengo a hablar de bloc, posiblemente unos de los gestores de estado mas utilizados en las aplicaciones construidas con flutter. Aunque al principio pueda resultar mas lioso que otros gestores de estado como provider o singleton, con el tiempo vais a ver más sus virtudes que sus inconvenientes. Espero poder explicarlo de manera clara (eso si va a ser complicado).

Let’s go!

Aplicación de prueba.

Como ejemplo vamos a hacer la misma aplicación de contador que hicimos en el ejemplo del patrón provider, que puedes leer aquí!

El código fuente lo puedes descargar de mi GitHub desde aquí

¿Que es bloc?

Bloc es el acrónimo de business logic component algo así como componente de lógica de negocio. Nos permite manejar el estado de la aplicación separando la lógica de negocio de los componentes de la vista, haciendo nuestra aplicación más sencilla de mantener. Los dos conceptos importantes en bloc son: el estado y los eventos. Juntos nos permiten gestionar el flujo de información en nuestra aplicación. Podríamos definir el flujo de información a través de bloc de la siguiente manera:

Bloc captura un evento (por ejemplo al pulsar un botón), se modifica la información necesaria dentro del bloc y se emite un nuevo estado. Si no te ha quedado claro no te preocupes, espero que en los siguientes apartados, con algunos ejemplos de código, se resuelvan tus dudas.

Instalación.

El primer paso es instalar los paquetes necesarios para gestionar bloc. Para ello vamos a ayudarnos de una librería que como siempre puedes encontrar en https://pub.dev

flutter_bloc: ^7.0.0

Creando nuestro primer bloc.

Aunque podemos gestionar todo el estado de nuestra aplicación desde un único bloc, lo ideal es dividir las funcionalidades de la aplicación en distintos bloc, de este modo hacemos nuestra aplicación más sencilla de comprender y mantener. Un bloc lo componen tres archivos ( _bloc, _event, _state). Aunque podemos crear esta estructura a mano, si utilizamos Visual Studio Code, nos podemos ayudar de un plugin llamado bloc que crea esta estructura por nosotros. Puedes encontrarlo aquí

Una vez tenemos el plugin instalado crear un bloc es tan sencillo como pulsar con el botón secundario del ratón sobre el directorio donde queremos crearlo y seleccionar la opción Bloc: New Bloc. Esto nos crea toda la estructura de nuestro bloc.

Componentes del bloc.

Como vimos en el apartado anterior cada bloc está formado por tres ficheros. En este apartado vamos a ver con detalle cada uno de ellos.

_state.dart.

En este fichero definiremos el estado de nuestro bloc, es decir, el conjunto de propiedades que nuestro bloc va a modificar y emitir en función de los eventos gestionados. Creo que con un ejemplo esto se puede ver mucho mejor.

part of 'counter_bloc.dart';

@immutable
class CounterState {
  final int counter;

  CounterState({
    this.counter
  });

  CounterState initialState() => new CounterState(counter: 0);

  CounterState copyWith({
    int counter
  }) => CounterState(
    counter: counter ?? this.counter
  );
}

En primer lugar vemos que este fichero está definido como un part of. Mediante esta sintaxis simplemente decimos que este fichero forma parte de otro, esto nos ayuda a la limpieza de código ya que de esta manera cuando queramos usar este bloc tenemos que importar un fichero en lugar de tres.

La clase está anotada como @inmutable. Este punto es muy importante, ya que bloc no modifica el estado sino que emite uno nuevo.

Dentro del cuerpo de la clase tenemos las propiedades que va a gestionar nuestro state. Es útil (aunque no obligatorio) definir un estado inicial. Esto nos ayuda a dar un valor por defecto a las variables una vez que se inicializa el bloc.

Por ultimo nos encontramos con el copyWith. Este método nos ayuda a generar un nuevo estado partiendo de un estado anterior, modificando solo las propiedades que requiramos. En este ejemplo en concreto no parece muy útil ya que solo tenemos una propiedad, pero imaginemos por un momento que el state tiene 15 propiedades y que queremos emitir un nuevo estado modificando solamente una de ellas con respecto al estado anterior, en ese caso si sería muy útil este método.

_event.dart.

En este fichero definiremos los eventos que va a gestionar nuestro bloc. Los eventos los podríamos definir como las acciones que se van a ejecutar desde componentes de la UI. Veamos un ejemplo.

part of 'counter_bloc.dart';

@immutable
abstract class CounterEvent {}

class OnCounterIncrement extends CounterEvent{}

class OnCounterDecrease extends CounterEvent{}

class OnCounterRestart extends CounterEvent{}

Como puedes ver tenemos varias clases definidas, cada una de ellas extiende de la clase abstracta CounterEvent. Este aspecto es muy relevante ya que de esta manera nos aseguramos que solo los eventos que hereden de esta clase serán aquellos que va a gestionar nuestro bloc. Las clases a su vez hacen referencia a acciones. La nomenclatura On… no es de obligado cumplimiento, aunque ya que estas clases hacen referencia a «acciones» es una buena forma de definirlas (recuerdan un poco a los eventos de javascript: onclick, onkeypress, onblur…)

_bloc.dart

Podríamos decir que este fichero es el cerebro del bloc. En el los estados y los eventos cobran sentido. Vamos a ver un ejemplo y luego comentamos detalles.

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(counter: 0));

  @override
  Stream<CounterState> mapEventToState(CounterEvent event) async* {
    if(event is OnCounterIncrement){
      yield state.copyWith(counter: state.counter + 1 );
    }else if(event is OnCounterDecrease){
      yield state.copyWith(counter: state.counter - 1);
    }else if(event is OnCounterRestart){
      yield state.initialState();
    }
  }
}

En la definición del constructor indicamos el estado inicial de nuestro bloc. Es decir los valores iniciales que se van a cargar cuando se cree el bloc.

El único método de la clase es mapEventToState heredado de la librería Bloc. Nos permite controlar todos los eventos posibles que hereden de nuestra clase mapEventToState. Puede parecer extraño el async* (no confundir con async). Mientras que async nos indica que el método que lo contiene resuelve un Future, async* nos indica que es una función generadora, dicho de otro modo, una función que retorna un stream. Ese retorno se hace mediante el yield. Para que nos hagamos una idea el yield es como un return, pero en lugar de retornar un valor, emites un stream, en este caso ese stream es un nuevo estado de nuestro bloc.

Consumir el bloc. MultiBlocProvider

Llegados a este punto ya tenemos todo nuestro bloc configurado, es hora de interactuar con él y modificar nuestra UI en función a los estados que tiene nuestro bloc. El primer paso es encapsular aquellos componentes del árbol de widgets que van a escuchar los cambios de nuestro bloc. Si bien es posible colocar nuestro bloc en una parte muy concreta de nuestra aplicación (sobre todo en grandes aplicaciones con múltiples bloc), en este caso y por no complicarnos mucho la vida lo vamos a colocar en el nivel mas alto de nuestro árbol de widgets. Para ello vamos a encapsular nuestro MaterialApp con un MultiBlocProvider que va a contener el listado de nuestros bloc, de la siguiente manera

MultiBlocProvider(
   providers: [
      BlocProvider(create: (_) => CounterBloc()),
   ],
   child:....
);

Desde este momento todos los widgets que sean hijos de MultiBlocProvider van a ser candidatos a escuchar cambios del bloc.

Añadir eventos al bloc.

Como bloc actualiza el estado en base a eventos, vamos a ver cómo añadir un evento. Primero vemos el código de ejemplo y luego comentamos los detalles.

FloatingActionButton(
  child: Icon(Icons.remove),
  onPressed: (){
    final counterBloc = BlocProvider.of<CounterBloc>(context);
    counterBloc.add(OnCounterDecrease());
  }
),

Obtenemos el bloc desde el context de la aplicación, vamos a buscar en nuestro BlocProvider un bloc de tipo CounterBloc y vamos a añadirle .add un evento de tipo OnCounterDecrease. De este modo actuamos sobre el.

Consumir el bloc. BlocBuilder

Por ultimo vamos a ver otra forma bastante común de actuar sobre nuestro bloc y es mediante el widget BlocBuilder que como su propio nombre indica, construye un widget en base a los cambios de estados generados por un bloc. Como siempre, primero ejemplo y luego comentamos.

BlocBuilder<CounterBloc, CounterState>(
   builder: (context, state) {
      return Center(
         child: Text('Has pulsado el botón: ${state.counter} veces')
       );
   },
),

Este BlocBuilder va a escuchar permanentemente los cambios en el bloc y va a reconstruir el widget en cada cambio de estado. Cada vez que lo reconstruye pinta la variable counter del nuevo estado.

Conclusiones.

Bueno pues hasta aquí este pequeño ejemplo de bloc. El tema da para mucho más y seguramente en otro post retome el tema con ejemplos más complejos y detallados. A simple vista puede parecer mucho código y muy complicado para hacer un simple contador, pero en grandes aplicaciones con una lógica de negocio más compleja, es donde se ve realmente el potencial de bloc.

Espero que os haya gustado el tema y os sea útil. Nos vemos en el siguiente post.

Bye!! 🙂