Saltearse al contenido

Conceptos de Bloc

Hay varios conceptos clave que son críticos para entender cómo usar el paquete bloc.

En las próximas secciones, vamos a discutir cada uno de ellos en detalle y también trabajaremos en cómo se aplicarían a una aplicación de contador.

Streams

Un stream es una secuencia de datos asíncronos.

Para usar la biblioteca bloc, es fundamental tener una comprensión básica de los Streams y cómo funcionan.

Si no estás familiarizado con los Streams, piensa en una tubería con agua fluyendo a través de ella. La tubería es el Stream y el agua son los datos asíncronos.

Podemos crear un Stream en Dart escribiendo una función async* (generador asíncrono).

count_stream.dart
Stream<int> countStream(int max) async* {
for (int i = 0; i < max; i++) {
yield i;
}
}

Al marcar una función como async* podemos usar la palabra clave yield y devolver un Stream de datos. En el ejemplo anterior, estamos devolviendo un Stream de enteros hasta el parámetro entero max.

Cada vez que usamos yield en una función async* estamos empujando ese dato a través del Stream.

Podemos consumir el Stream anterior de varias maneras. Si quisiéramos escribir una función para devolver la suma de un Stream de enteros, podría verse algo así:

sum_stream.dart
Future<int> sumStream(Stream<int> stream) async {
int sum = 0;
await for (int value in stream) {
sum += value;
}
return sum;
}

Al marcar la función anterior como async podemos usar la palabra clave await y devolver un Future de enteros. En este ejemplo, estamos esperando cada valor en el stream y devolviendo la suma de todos los enteros en el stream.

Podemos juntar todo de la siguiente manera:

main.dart
void main() async {
/// Initialize a stream of integers 0-9
Stream<int> stream = countStream(10);
/// Compute the sum of the stream of integers
int sum = await sumStream(stream);
/// Print the sum
print(sum); // 45
}

Ahora que tenemos una comprensión básica de cómo funcionan los Streams en Dart, estamos listos para aprender sobre el componente principal del paquete bloc: un Cubit.

Cubit

Un Cubit es una clase que extiende BlocBase y puede ser extendida para gestionar cualquier tipo de estado.

Arquitectura de Cubit

Un Cubit puede exponer funciones que pueden ser invocadas para desencadenar cambios de estado.

Los estados son la salida de un Cubit y representan una parte del estado de tu aplicación. Los componentes de la interfaz de usuario pueden ser notificados de los estados y redibujar partes de sí mismos en función del estado actual.

Creando un Cubit

Podemos crear un CounterCubit así:

counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
}

Cuando creamos un Cubit, necesitamos definir el tipo de estado que el Cubit gestionará. En el caso del CounterCubit anterior, el estado puede ser representado mediante un int, pero en casos más complejos podría ser necesario usar una class en lugar de un tipo primitivo.

La segunda cosa que necesitamos hacer al crear un Cubit es especificar el estado inicial. Podemos hacer esto llamando a super con el valor del estado inicial. En el fragmento anterior, estamos configurando el estado inicial a 0 internamente, pero también podemos permitir que el Cubit sea más flexible aceptando un valor externo:

counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}

Esto nos permitiría instanciar CounterCubit con diferentes estados iniciales como:

main.dart
final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10

Cambios de Estado en Cubit

Cada Cubit tiene la capacidad de emitir un nuevo estado mediante emit.

counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}

En el fragmento anterior, el CounterCubit está exponiendo un método público llamado increment que puede ser llamado externamente para notificar al CounterCubit que incremente su estado. Cuando se llama a increment, podemos acceder al estado actual del Cubit mediante el getter state y emitir un nuevo estado sumando 1 al estado actual.

Usando un Cubit

Ahora podemos tomar el CounterCubit que hemos implementado y ponerlo en uso.

Uso Básico

main.dart
void main() {
final cubit = CounterCubit();
print(cubit.state); // 0
cubit.increment();
print(cubit.state); // 1
cubit.close();
}

En el fragmento anterior, comenzamos creando una instancia del CounterCubit. Luego imprimimos el estado actual del cubit, que es el estado inicial (ya que no se han emitido nuevos estados aún). A continuación, llamamos a la función increment para desencadenar un cambio de estado. Finalmente, imprimimos el estado del Cubit nuevamente, que pasó de 0 a 1 y llamamos a close en el Cubit para cerrar el stream interno de estado.

Uso de Stream

Cubit expone un Stream que nos permite recibir actualizaciones de estado en tiempo real:

main.dart
Future<void> main() async {
final cubit = CounterCubit();
final subscription = cubit.stream.listen(print); // 1
cubit.increment();
await Future.delayed(Duration.zero);
await subscription.cancel();
await cubit.close();
}

En el fragmento anterior, nos estamos suscribiendo al CounterCubit y llamando a imprimir en cada cambio de estado. Luego invocamos la función increment que emitirá un nuevo estado. Por último, llamamos a cancel en la suscripción cuando ya no queremos recibir actualizaciones y cerramos el Cubit.

Observando un Cubit

Cuando un Cubit emite un nuevo estado, ocurre un Change. Podemos observar todos los cambios para un Cubit dado sobrescribiendo onChange.

counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}

Luego podemos interactuar con el Cubit y observar todos los cambios impresos en la consola.

main.dart
void main() {
CounterCubit()
..increment()
..close();
}

El ejemplo anterior imprimiría:

Ventana de terminal
Change { currentState: 0, nextState: 1 }

BlocObserver

Una ventaja adicional de usar la biblioteca bloc es que podemos tener acceso a todos los Changes en un solo lugar. Aunque en esta aplicación solo tenemos un Cubit, es bastante común en aplicaciones más grandes tener muchos Cubits gestionando diferentes partes del estado de la aplicación.

Si queremos poder hacer algo en respuesta a todos los Changes, simplemente podemos crear nuestro propio BlocObserver.

simple_bloc_observer.dart
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}

Para usar el SimpleBlocObserver, solo necesitamos ajustar la función main:

main.dart
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}

El fragmento anterior imprimiría:

Ventana de terminal
CounterCubit Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Manejo de Errores en Cubit

Cada Cubit tiene un método addError que puede ser usado para indicar que ha ocurrido un error.

counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}

onError también puede ser sobrescrito en BlocObserver para manejar todos los errores reportados globalmente.

simple_bloc_observer.dart
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}

Si ejecutamos el mismo programa nuevamente, deberíamos ver la siguiente salida:

Ventana de terminal
Exception: increment error!
#0 CounterCubit.increment (file:///main.dart:7:56)
#1 main (file:///main.dart:41:7)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
CounterCubit Exception: increment error!
#0 CounterCubit.increment (file:///main.dart:7:56)
#1 main (file:///main.dart:41:7)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
CounterCubit Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Bloc

Un Bloc es una clase más avanzada que se basa en eventos para desencadenar cambios de estado en lugar de funciones. Bloc también extiende BlocBase, lo que significa que tiene una API pública similar a Cubit. Sin embargo, en lugar de llamar a una función en un Bloc y emitir directamente un nuevo estado, los Blocs reciben eventos y convierten los eventos entrantes en estados salientes.

Arquitectura de Bloc

Creando un Bloc

Crear un Bloc es similar a crear un Cubit, excepto que además de definir el estado que gestionaremos, también debemos definir el evento que el Bloc podrá procesar.

Los eventos son la entrada a un Bloc. Comúnmente se agregan en respuesta a interacciones del usuario, como presiones de botones o eventos de ciclo de vida como cargas de página.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
}

Al igual que cuando creamos el CounterCubit, debemos especificar un estado inicial pasándolo a la superclase a través de super.

Cambios de Estado en Bloc

Bloc requiere que registremos manejadores de eventos a través de la API on<Event>, a diferencia de las funciones en Cubit. Un manejador de eventos es responsable de convertir cualquier evento entrante en cero o más estados salientes.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
// handle incoming `CounterIncrementPressed` event
});
}
}

Luego podemos actualizar el EventHandler para manejar el evento CounterIncrementPressed:

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
emit(state + 1);
});
}
}

En el fragmento anterior, hemos registrado un EventHandler para gestionar todos los eventos CounterIncrementPressed. Para cada evento CounterIncrementPressed entrante, podemos acceder al estado actual del bloc a través del getter state y emit(state + 1).

Usando un Bloc

En este punto, podemos crear una instancia de nuestro CounterBloc y ponerlo en uso.

Uso Básico

main.dart
Future<void> main() async {
final bloc = CounterBloc();
print(bloc.state); // 0
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
print(bloc.state); // 1
await bloc.close();
}

En el fragmento anterior, comenzamos creando una instancia del CounterBloc. Luego imprimimos el estado actual del Bloc, que es el estado inicial (ya que no se han emitido nuevos estados aún). A continuación, agregamos el evento CounterIncrementPressed para desencadenar un cambio de estado. Finalmente, imprimimos el estado del Bloc nuevamente, que pasó de 0 a 1 y llamamos a close en el Bloc para cerrar el stream interno de estado.

Uso de Stream

Al igual que con Cubit, un Bloc es un tipo especial de Stream, lo que significa que también podemos suscribirnos a un Bloc para recibir actualizaciones en tiempo real de su estado:

main.dart
Future<void> main() async {
final bloc = CounterBloc();
final subscription = bloc.stream.listen(print); // 1
bloc.add(CounterIncrementPressed());
await Future.delayed(Duration.zero);
await subscription.cancel();
await bloc.close();
}

En el fragmento anterior, nos estamos suscribiendo al CounterBloc y llamando a imprimir en cada cambio de estado. Luego agregamos el evento CounterIncrementPressed que desencadena el EventHandler on<CounterIncrementPressed> y emite un nuevo estado. Por último, llamamos a cancel en la suscripción cuando ya no queremos recibir actualizaciones y cerramos el Bloc.

Observando un Bloc

Dado que Bloc extiende BlocBase, podemos observar todos los cambios de estado para un Bloc usando onChange.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}

Luego podemos actualizar main.dart a:

main.dart
void main() {
CounterBloc()
..add(CounterIncrementPressed())
..close();
}

Ahora, si ejecutamos el fragmento anterior, la salida será:

Ventana de terminal
Change { currentState: 0, nextState: 1 }

Un factor diferenciador clave entre Bloc y Cubit es que, dado que Bloc está basado en eventos, también podemos capturar información sobre lo que desencadenó el cambio de estado.

Podemos hacer esto sobrescribiendo onTransition.

El cambio de un estado a otro se llama Transition. Una Transition consiste en el estado actual, el evento y el siguiente estado.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
super.onTransition(transition);
print(transition);
}
}

Si luego volvemos a ejecutar el mismo fragmento main.dart de antes, deberíamos ver la siguiente salida:

Ventana de terminal
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
Change { currentState: 0, nextState: 1 }

BlocObserver

Al igual que antes, podemos sobrescribir onTransition en un BlocObserver personalizado para observar todas las transiciones que ocurren desde un solo lugar.

simple_bloc_observer.dart
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType} $transition');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('${bloc.runtimeType} $error $stackTrace');
super.onError(bloc, error, stackTrace);
}
}

Podemos inicializar el SimpleBlocObserver de la misma manera que antes:

main.dart
void main() {
Bloc.observer = SimpleBlocObserver();
CounterBloc()
..add(CounterIncrementPressed())
..close();
}

Ahora, si ejecutamos el fragmento anterior, la salida debería verse así:

Ventana de terminal
CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Otra característica única de las instancias de Bloc es que nos permiten sobrescribir onEvent, que se llama cada vez que se agrega un nuevo evento al Bloc. Al igual que con onChange y onTransition, onEvent puede ser sobrescrito localmente así como globalmente.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onEvent(CounterEvent event) {
super.onEvent(event);
print(event);
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
super.onTransition(transition);
print(transition);
}
}
simple_bloc_observer.dart
class SimpleBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
print('${bloc.runtimeType} $event');
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('${bloc.runtimeType} $transition');
}
}

Podemos ejecutar el mismo main.dart de antes y deberíamos ver la siguiente salida:

Ventana de terminal
CounterBloc Instance of 'CounterIncrementPressed'
Instance of 'CounterIncrementPressed'
CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Manejo de Errores en Bloc

Al igual que con Cubit, cada Bloc tiene un método addError y onError. Podemos indicar que ha ocurrido un error llamando a addError desde cualquier lugar dentro de nuestro Bloc. Luego podemos reaccionar a todos los errores sobrescribiendo onError al igual que con Cubit.

counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
addError(Exception('increment error!'), StackTrace.current);
emit(state + 1);
});
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition);
super.onTransition(transition);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}

Si volvemos a ejecutar el mismo main.dart de antes, podemos ver cómo se ve cuando se informa un error:

Ventana de terminal
Exception: increment error!
#0 new CounterBloc.<anonymous closure> (file:///main.dart:10:58)
#1 Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:229:26)
#2 Bloc.on.<anonymous closure> (package:bloc/src/bloc.dart:238:9)
#3 _MapStream._handleData (dart:async/stream_pipe.dart:213:31)
#4 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#5 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#6 CastStreamSubscription._onData (dart:_internal/async_cast.dart:85:11)
#7 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#8 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#9 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#10 _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
#11 _WhereStream._handleData (dart:async/stream_pipe.dart:195:12)
#12 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#13 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#14 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#15 _DelayedData.perform (dart:async/stream_impl.dart:515:14)
#16 _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11)
#17 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:591:7)
#18 _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#19 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#20 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#21 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)
CounterBloc Exception: increment error!
#0 new CounterBloc.<anonymous closure> (file:///main.dart:10:58)
#1 Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:229:26)
#2 Bloc.on.<anonymous closure> (package:bloc/src/bloc.dart:238:9)
#3 _MapStream._handleData (dart:async/stream_pipe.dart:213:31)
#4 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#5 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#6 CastStreamSubscription._onData (dart:_internal/async_cast.dart:85:11)
#7 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#8 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#9 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#10 _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
#11 _WhereStream._handleData (dart:async/stream_pipe.dart:195:12)
#12 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
#13 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#14 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#15 _DelayedData.perform (dart:async/stream_impl.dart:515:14)
#16 _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11)
#17 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:591:7)
#18 _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#19 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
#20 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13)
#21 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5)
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
CounterBloc Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Cubit vs. Bloc

Ahora que hemos cubierto los conceptos básicos de las clases Cubit y Bloc, podrías preguntarte cuándo deberías usar Cubit y cuándo deberías usar Bloc.

Ventajas de Cubit

Simplicidad

Una de las mayores ventajas de usar Cubit es la simplicidad. Al crear un Cubit, solo tenemos que definir el estado así como las funciones que queremos exponer para cambiar el estado. En comparación, al crear un Bloc, tenemos que definir los estados, eventos y la implementación del EventHandler. Esto hace que Cubit sea más fácil de entender y hay menos código involucrado.

Ahora echemos un vistazo a las dos implementaciones del contador:

CounterCubit
counter_cubit.dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
CounterBloc
counter_bloc.dart
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
}

La implementación de Cubit es más concisa y en lugar de definir eventos por separado, las funciones actúan como eventos. Además, al usar un Cubit, podemos simplemente llamar a emit desde cualquier lugar para desencadenar un cambio de estado.

Ventajas de Bloc

Rastreabilidad

Una de las mayores ventajas de usar Bloc es conocer la secuencia de cambios de estado así como exactamente qué desencadenó esos cambios. Para el estado que es crítico para la funcionalidad de una aplicación, podría ser muy beneficioso usar un enfoque más basado en eventos para capturar todos los eventos además de los cambios de estado.

Un caso de uso común podría ser gestionar el AuthenticationState. Para simplificar, digamos que podemos representar el AuthenticationState a través de un enum:

authentication_state.dart
enum AuthenticationState { unknown, authenticated, unauthenticated }

Podría haber muchas razones por las cuales el estado de la aplicación podría cambiar de authenticated a unauthenticated. Por ejemplo, el usuario podría haber tocado un botón de cierre de sesión y solicitado ser desconectado de la aplicación. Por otro lado, tal vez el token de acceso del usuario fue revocado y fue desconectado forzosamente. Al usar Bloc podemos rastrear claramente cómo el estado de la aplicación llegó a un cierto estado.

Ventana de terminal
Transition {
currentState: AuthenticationState.authenticated,
event: LogoutRequested,
nextState: AuthenticationState.unauthenticated
}

La Transition anterior nos da toda la información que necesitamos para entender por qué cambió el estado. Si hubiéramos usado un Cubit para gestionar el AuthenticationState, nuestros registros se verían así:

Ventana de terminal
Change {
currentState: AuthenticationState.authenticated,
nextState: AuthenticationState.unauthenticated
}

Esto nos dice que el usuario fue desconectado pero no explica por qué, lo cual podría ser crítico para depurar y entender cómo está cambiando el estado de la aplicación con el tiempo.

Transformaciones Avanzadas de Eventos

Otra área en la que Bloc sobresale sobre Cubit es cuando necesitamos aprovechar operadores reactivos como buffer, debounceTime, throttle, etc.

Bloc tiene un sink de eventos que nos permite controlar y transformar el flujo entrante de eventos.

Por ejemplo, si estuviéramos construyendo una búsqueda en tiempo real, probablemente querríamos aplicar debounce a las solicitudes al backend para evitar ser limitados en la tasa de solicitudes, así como para reducir el costo/carga en el backend.

Con Bloc podemos proporcionar un EventTransformer personalizado para cambiar la forma en que los eventos entrantes son procesados por el Bloc.

counter_bloc.dart
EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
CounterBloc() : super(0) {
on<Increment>(
(event, emit) => emit(state + 1),
/// Apply the custom `EventTransformer` to the `EventHandler`.
transformer: debounce(const Duration(milliseconds: 300)),
);
}

Con el código anterior, podemos aplicar fácilmente debounce a los eventos entrantes con muy poco código adicional.

Si no estás seguro de cuál usar, comienza con Cubit y luego puedes refactorizar o escalar a un Bloc según sea necesario.