Conceitos do Bloc
Existem vários conceitos básicos que são essenciais para entender como usar o pacote bloc.
Nas próximas seções, discutiremos cada um deles em detalhes e veremos como eles se aplicariam a um aplicativo de contador.
Um stream é uma sequência de dados asincronos.
Para usar a biblioteca bloc, é fundamental ter um compreensão básica de Streams
e como eles funcionam.
Se você não está familiarizado com Streams
, basta pensar em um tubo com água fluindo por dele. O tubo é o Stream
e a água são os dados assíncronos.
Nós podemos criar um Stream
em Dart escrevendo uma função async*
(gerador de async).
Stream<int> countStream(int max) async* { for (int i = 0; i < max; i++) { yield i; }}
Marcando a função como async*
podemos usar a palavra-chave yield
para retornar um Stream
de dados. No exemplo acima, retornamos um Stream
de inteiros até o parâmetro inteiro max
.
Toda vez que colocamos um yield
em uma função async*
estamos inserindo esse pedaço de dados no Stream
.
Podemos consumir o Stream
acima em várias formas. Se quisermos escrever uma função para retornar a soma de um Stream
de inteiros, poderia escrever algo como:
Future<int> sumStream(Stream<int> stream) async { int sum = 0; await for (int value in stream) { sum += value; } return sum;}
Marcando a função acima como async
podemos usar a palavra-chave await
para retornar um Future
de inteiros. Neste exemplo, esperamos cada valor do stream e retornamos a soma de todos os inteiros no stream.
Podemos juntar todo isso assim:
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}
Agora que temos uma compreensão básica de como Streams
funcionam em Dart, estamos prontos para aprender sobre o componente central do pacote bloc: o Cubit
.
Um Cubit
é uma classe que estende BlocBase
e pode ser estendida para gerenciar qualquer tipo de estado.
Um Cubit
pode expor funções que podem ser chamadas para disparar mudanças de estado.
Estados são a saida de um Cubit
e representam uma parte do estado da sua aplicação. Os componentes de IU podem ser notificados pelos estados e se redesenharem com base no estado atual.
Podemos criar um CounterCubit
como:
class CounterCubit extends Cubit<int> { CounterCubit() : super(0);}
Ao criar um Cubit
, precisamos definir o tipo de estado que o Cubit
vai gerenciar. No caso do CounterCubit
acima, o estado é representado por um int
mas em casos mais complexos, pode ser necessário usar uma class
ao invés de um tipo primitivo.
A segunda coisa que precisamos fazer ao criar um Cubit
é especificar o estado inicial. Podemos fazer isso chamando super
com o valor do estado inicial. No exemplo acima, definimos o estado inicial para 0
internamente, mas também podemos permitir que o Cubit
seja mais flexível aceitando um valor externo:
class CounterCubit extends Cubit<int> { CounterCubit(int initialState) : super(initialState);}
Isto nos permitiria criar instâncias de CounterCubit
com diferentes estados iniciais, como:
final cubitA = CounterCubit(0); // state starts at 0final cubitB = CounterCubit(10); // state starts at 10
Cada Cubit
tem a capacidade de emitir um novo estado via emit
.
class CounterCubit extends Cubit<int> { CounterCubit() : super(0);
void increment() => emit(state + 1);}
No trecho acima, o CouterCubit
está expondo um método publico chamado increment
que pode ser chamado externamente para notificar o CounterCubit
para incrementar seu estado. Quando increment
é chamado, podemos acessar o estado atual do Cubit
através do getter state
e emit
um novo estado adicionando 1 ao estado atual.
Agora podemos pegar o CounterCubit
implementado e coloca-lo em uso!
void main() { final cubit = CounterCubit(); print(cubit.state); // 0 cubit.increment(); print(cubit.state); // 1 cubit.close();}
No trecho acima, começamos criando uma instância de um CounterCubit
. Em seguida, imprimimos o estado atual do cubit, que é o estado inicial (já que nenhum estado novo foi emitido ainda). Em seguida, chamamos a função increment
para disparar uma mudança de estado. Por fim, imprimimos o estado do Cubit
novamente que passou de 0
para 1
e chamamos close
no Cubit
para fechar o fluxo de dados interno.
Cubit
expõe um Stream
que nos permite receber atualizações de estado em tempo 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();}
No trecho acima, estamos assinando o CounterCubit
e chamando print a cada mudança de estado. Em seguida, invocamos a função increment
que emitirá um novo estado. Por fim, estamos chamando cancel
na subscription
quando não queremos mais receber atualizações e fechando o Cubit
.
Quando um Cubit
emite um novo estado, ocorre uma Change
. Podemos observar todas as mudanças em um Cubit
substituindo 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); }}
Podemos então interagir com o Cubit
e observar todas as alterações geradas no console.
void main() { CounterCubit() ..increment() ..close();}
O exemplo acima produziria:
Change { currentState: 0, nextState: 1 }
Um bônus adicional de usar a biblioteca bloc é que podemos ter acesso a todas as Changes
em um lugar. Embora nessa aplicação tenhamos apenas um Cubit
, é muito comum em aplicações maiores ter muitos Cubits
gerenciando diferentes partes do estado da aplicação.
Se quisermos fazer algo em resposta a todas as Changes
, podemos simplesmente criar nosso próprio BlocObserver
.
class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('${bloc.runtimeType} $change'); }}
Para usar o SimpleBlocObserver
, precisamos apenas ajustar a função main
:
void main() { Bloc.observer = SimpleBlocObserver(); CounterCubit() ..increment() ..close();}
O trecho acima produziria então:
CounterCubit Change { currentState: 0, nextState: 1 }Change { currentState: 0, nextState: 1 }
Todo Cubit
tem um método addError
que pode ser usado para indicar que ocorreu um erro.
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
também pode ser sobrescrito no BlocObserver
para manipular todos os erros relatados 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); }}
Se rodarmos o mesmo programa novamente, devemos ver o seguinte output:
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 }
Um Bloc
é uma classe mais avançada que depende de eventos
para acionar mudanças de estado
em vez de funções. Bloc
também estende BlocBase
, o que significa que ele tem uma API pública semelhante ao Cubit
. No entanto, em vez de chamar uma função
em um Bloc
e emitir diretamente um novo estado
, os Blocs
recebem eventos
e convertem os eventos
de entrada em estados
de saída.
Criar um Bloc
é semelhante a criar um Cubit
, exceto que além de definir o estado que iremos gerenciar, também devemos definir o evento que o Bloc
poderá processar.
Eventos são a entrada para um Bloc. Eles normalmente são adicionados em resposta a interações do usuário, como botões pressionados ou eventos do ciclo de vida, como carregamentos de página.
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0);}
Assim como ao criar o CounterCubit
, devemos especificar um estado inicial passando-o para a superclasse via super
.
Bloc
exige que registremos manipuladores de eventos via a API on<Event>
, em vez de funções no Cubit
. Um manipulador de eventos é responsável por converter quaisquer eventos de entrada em zero ou mais estados de saída.
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 }); }}
Podemos então atualizar o EventHandler
para lidar com o 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); }); }}
No trecho acima, registramos um EventHandler
para gerenciar todos os eventos CounterIncrementPressed
. Para cada evento CounterIncrementPressed
recebido, podemos acessar o estado atual do bloc via o getter state
e emit(state + 1)
.
Neste ponto, podemos criar uma instância do nosso CounterBloc
e colocá-lo em 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();}
No trecho acima, começamos criando uma instância do nosso CounterBloc
. Em seguida, imprimimos o estado atual do Bloc
que é o estado inicial (já que nenhum estado novo foi emitido ainda). Em seguida, adicionamos o evento CounterIncrementPressed
para disparar uma mudança de estado. Por fim, imprimimos o estado do Bloc
novamente que foi de 0
para 1
e chamamos close
no Bloc
para fechar o fluxo de dados interno.
Assim como com Cubit
, um Bloc
é um tipo especial de Stream
, o que significa que também podemos assinar um Bloc
para atualizações em tempo real de seu 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();}
No trecho acima, estamos assinando o CounterBloc
e chamando print a cada mudança de estado. Em seguida, adicionamos o evento CounterIncrementPressed
, que aciona o EventHandler
on<CounterIncrementPressed>
e emite um novo estado. Por fim, estamos chamando o cancel
da assinatura quando não queremos mais receber atualizações e fechando o Bloc
.
Como o Bloc
estende BlocBase
, podemos observar todas as mudanças de estado de um 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); }}
Podemos então atualizar main.dart
para:
void main() { CounterBloc() ..add(CounterIncrementPressed()) ..close();}
Agora, se executarmos o trecho acima, a saída será:
Change { currentState: 0, nextState: 1 }
Um fator-chave de diferenciação entre Bloc
e Cubit
é que, como o Bloc
é orientado a eventos, nós também podemos capturar informações sobre o que disparou a mudança de estado.
Podemos fazer isso substituindo onTransition
.
A mudança de um estado para outro é chamada de Transition
. Uma Transition
consiste no estado atual, no evento e no próximo 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); }}
Se executarmos novamente o mesmo trecho main.dart
de antes, veremos a seguinte saída:
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }Change { currentState: 0, nextState: 1 }
Assim como antes, podemos substituir o onTransition
em um BlocObserver
personalizado para observar todas as transições que ocorrem em um único 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 o SimpleBlocObserver
como antes:
void main() { Bloc.observer = SimpleBlocObserver(); CounterBloc() ..add(CounterIncrementPressed()) ..close();}
Agora, se executarmos o trecho acima, a saída será semelhante a:
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 }
Outro recurso exclusivo das instâncias do Bloc
é que elas nos permitem sobrescrever onEvent
, que é chamado sempre que um novo evento é adicionado ao Bloc
. Assim como com onChange
e onTransition
, onEvent
pode ser sobrescrito localmente e também 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 executar o mesmo main.dart
de antes e devemos ver a seguinte saída:
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 }
Assim como no Cubit
, cada Bloc
tem um método addError
e onError
. Podemos indicar que ocorreu um erro chamando addError
de qualquer lugar dentro do nosso Bloc
. Podemos então reagir a todos os erros sobrescrevendo onError
assim como no 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); }}
Se executarmos novamente o mesmo main.dart
de antes, podemos ver como fica quando um erro é reportado:
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 }
Agora que cobrimos os conceitos básicos das classes Cubit
e Bloc
, você pode estar se perguntando quando deve usar Cubit
e quando deve usar Bloc
.
Uma das maiores vantagens de usar o Cubit
é a simplicidade. Ao criar um Cubit
, precisamos apenas definir o estado, bem como as funções que queremos expor para alterar o estado. Em comparação, ao criar um Bloc
, temos de definir os estados, os eventos e a implementação do EventHandler
. Isso torna o Cubit mais fácil de entender e há menos código envolvido.
Agora vamos dar uma olhada nas duas implementações do 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)); }}
A implementação do Cubit
é mais concisa e, em vez de definir eventos separadamente, as funções agem como eventos. Além disso, ao usar um Cubit
, podemos simplesmente chamar emit
de qualquer lugar para disparar uma mudança de estado.
Uma das maiores vantagens de usar o Bloc
é conhecer a sequência de alterações de estado, bem como o que exatamente desencadeou essas alterações. Para o estado que é essencial para a funcionalidade de um aplicativo, pode ser muito vantajoso usar uma abordagem mais orientada a eventos para capturar todos os eventos, além das mudanças de estado.
Um caso de uso comum pode ser gerenciar AuthenticationState
. Para simplificar, digamos que podemos representar AuthenticationState
por meio de um enum
:
enum AuthenticationState { unknown, authenticated, unauthenticated }
Pode haver muitos motivos pelos quais o estado do aplicativo pode mudar de authenticated
para não unauthenticated
. Por exemplo, o usuário pode ter tocado no botão de logout e solicitado que fosse desconectado do aplicativo. Por outro lado, talvez o token de acesso do usuário tenha sido revogado e ele foi desconectado à força. Ao usar o Bloc
, podemos rastrear claramente como o estado do aplicativo chegou a um determinado valor.
Transition { currentState: AuthenticationState.authenticated, event: LogoutRequested, nextState: AuthenticationState.unauthenticated}
A Transition
acima nos dá todas as informações que precisamos para entender por que o estado mudou. Se tivéssemos usado um Cubit
para gerenciar o AuthenticationState
, nossos logs ficariam assim:
Change { currentState: AuthenticationState.authenticated, nextState: AuthenticationState.unauthenticated}
Isso nos diz que o usuário foi desconectado, mas não explica o motivo, o que pode ser crítico para a depuração e compreensão de como o estado do aplicativo está mudando ao longo do tempo.
Outra área em que o Bloc
se destaca sobre o Cubit
é quando precisamos tirar vantagem de operadores reativos, como buffer
, debounceTime
, throttle
, etc.
O Bloc
tem um coletor de eventos que nos permite controlar e transformar o fluxo de entrada de eventos.
Por exemplo, se estivéssemos criando uma pesquisa em tempo real, provavelmente desejaríamos reduzir as solicitações para o backend para evitar limitações de taxa e também para reduzir custos/carga no backend.
Com o Bloc
, podemos fornecer um EventTransformer
personalizado para alterar a maneira como os eventos recebidos são processados pelo 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)), );}
Com o código acima, podemos facilmente reduzir o retorno de eventos recebidos com muito pouco código adicional.
Se não tiver certeza sobre qual usar, comece com o Cubit
e depois refatore ou expanda para um Bloc
, conforme necessário.