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.
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).
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í:
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:
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
.
Un Cubit
es una clase que extiende BlocBase
y puede ser extendida para gestionar cualquier tipo de estado.
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.
Podemos crear un CounterCubit
así:
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:
class CounterCubit extends Cubit<int> { CounterCubit(int initialState) : super(initialState);}
Esto nos permitiría instanciar CounterCubit
con diferentes estados iniciales como:
final cubitA = CounterCubit(0); // state starts at 0final cubitB = CounterCubit(10); // state starts at 10
Cada Cubit
tiene la capacidad de emitir un nuevo estado mediante emit
.
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.
Ahora podemos tomar el CounterCubit
que hemos implementado y ponerlo en uso.
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.
Cubit
expone un Stream
que nos permite recibir actualizaciones de estado en tiempo real:
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
.
Cuando un Cubit
emite un nuevo estado, ocurre un Change
. Podemos observar todos los cambios para un Cubit
dado sobrescribiendo onChange
.
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.
void main() { CounterCubit() ..increment() ..close();}
El ejemplo anterior imprimiría:
Change { currentState: 0, nextState: 1 }
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
.
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
:
void main() { Bloc.observer = SimpleBlocObserver(); CounterCubit() ..increment() ..close();}
El fragmento anterior imprimiría:
CounterCubit Change { currentState: 0, nextState: 1 }Change { currentState: 0, nextState: 1 }
Cada Cubit
tiene un método addError
que puede ser usado para indicar que ha ocurrido un error.
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.
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:
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 }
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.
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.
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
.
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.
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
:
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)
.
En este punto, podemos crear una instancia de nuestro CounterBloc
y ponerlo en uso.
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.
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:
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
.
Dado que Bloc
extiende BlocBase
, podemos observar todos los cambios de estado para un Bloc
usando onChange
.
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:
void main() { CounterBloc() ..add(CounterIncrementPressed()) ..close();}
Ahora, si ejecutamos el fragmento anterior, la salida será:
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.
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:
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }Change { currentState: 0, nextState: 1 }
Al igual que antes, podemos sobrescribir onTransition
en un BlocObserver
personalizado para observar todas las transiciones que ocurren desde un solo lugar.
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:
void main() { Bloc.observer = SimpleBlocObserver(); CounterBloc() ..add(CounterIncrementPressed()) ..close();}
Ahora, si ejecutamos el fragmento anterior, la salida debería verse así:
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.
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); }}
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:
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 }
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
.
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:
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 }
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
.
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:
class CounterCubit extends Cubit<int> { CounterCubit() : super(0);
void increment() => emit(state + 1);}
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.
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
:
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.
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í:
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.
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
.
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.