Pular para o conteúdo

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.

Streams

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

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

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

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
}

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.

Cubit

Um Cubit é uma classe que estende BlocBase e pode ser estendida para gerenciar qualquer tipo de estado.

Cubit Architecture

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.

Criando um Cubit

Podemos criar um CounterCubit como:

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

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

Isto nos permitiria criar instâncias de CounterCubit com diferentes estados iniciais, como:

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

Mudanças de estado do Cubit

Cada Cubit tem a capacidade de emitir um novo estado via emit.

counter_cubit.dart
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.

Usando um Cubit

Agora podemos pegar o CounterCubit implementado e coloca-lo em uso!

Uso Básico

main.dart
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.

Uso de Stream

Cubit expõe um Stream que nos permite receber atualizações de estado em tempo 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();
}

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.

Observando um Cubit

Quando um Cubit emite um novo estado, ocorre uma Change. Podemos observar todas as mudanças em um Cubit substituindo 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);
}
}

Podemos então interagir com o Cubit e observar todas as alterações geradas no console.

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

O exemplo acima produziria:

Terminal window
Change { currentState: 0, nextState: 1 }

BlocObserver

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.

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 o SimpleBlocObserver, precisamos apenas ajustar a função main:

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

O trecho acima produziria então:

Terminal window
CounterCubit Change { currentState: 0, nextState: 1 }
Change { currentState: 0, nextState: 1 }

Tratamento de Erros do Cubit

Todo Cubit tem um método addError que pode ser usado para indicar que ocorreu um erro.

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 também pode ser sobrescrito no BlocObserver para manipular todos os erros relatados 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);
}
}

Se rodarmos o mesmo programa novamente, devemos ver o seguinte output:

Terminal window
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

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.

Bloc Architecture

Criando um Bloc

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.

counter_bloc.dart
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.

Mundanças de Estado do Bloc

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.

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
});
}
}

Podemos então atualizar o EventHandler para lidar com o 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);
});
}
}

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

Usando um Bloc

Neste ponto, podemos criar uma instância do nosso CounterBloc e colocá-lo em 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();
}

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.

Uso de Stream

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:

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();
}

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.

Observando um Bloc

Como o Bloc estende BlocBase, podemos observar todas as mudanças de estado de um 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);
}
}

Podemos então atualizar main.dart para:

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

Agora, se executarmos o trecho acima, a saída será:

Terminal window
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.

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);
}
}

Se executarmos novamente o mesmo trecho main.dart de antes, veremos a seguinte saída:

Terminal window
Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 }
Change { currentState: 0, nextState: 1 }

BlocObserver

Assim como antes, podemos substituir o onTransition em um BlocObserver personalizado para observar todas as transições que ocorrem em um único 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 o SimpleBlocObserver como antes:

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

Agora, se executarmos o trecho acima, a saída será semelhante a:

Terminal window
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.

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 executar o mesmo main.dart de antes e devemos ver a seguinte saída:

Terminal window
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 }

Tratamento de Erros no Bloc

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.

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);
}
}

Se executarmos novamente o mesmo main.dart de antes, podemos ver como fica quando um erro é reportado:

Terminal window
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

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.

Vantagens do Cubit

Simplicidade

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:

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));
}
}

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.

Vantagens do Bloc

Rastreabilidade

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:

authentication_state.dart
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.

Terminal window
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:

Terminal window
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.

Transformações Avançadas de Eventos

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.

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)),
);
}

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.