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
Seção intitulada “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).
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.
Criando um Cubit
Seção intitulada “Criando um Cubit”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
Mudanças de estado do Cubit
Seção intitulada “Mudanças de estado do Cubit”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.
Usando um Cubit
Seção intitulada “Usando um Cubit”Agora podemos pegar o CounterCubit
implementado e coloca-lo em uso!
Uso Básico
Seção intitulada “Uso Básico”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
Seção intitulada “Uso de Stream”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
.
Observando um Cubit
Seção intitulada “Observando um 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 }
BlocObserver
Seção intitulada “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
.
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 }
Tratamento de Erros do Cubit
Seção intitulada “Tratamento de Erros do Cubit”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.
Criando um Bloc
Seção intitulada “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.
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
Seção intitulada “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.
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)
.
Usando um Bloc
Seção intitulada “Usando um Bloc”Neste ponto, podemos criar uma instância do nosso CounterBloc
e colocá-lo em
uso!
Uso Básico
Seção intitulada “Uso Básico”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
Seção intitulada “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:
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
Seção intitulada “Observando um 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 }
BlocObserver
Seção intitulada “BlocObserver”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 }
Tratamento de Erros no Bloc
Seção intitulada “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
.
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 }
Cubit vs. Bloc
Seção intitulada “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
Seção intitulada “Vantagens do Cubit”Simplicidade
Seção intitulada “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
Seção intitulada “CounterCubit”class CounterCubit extends Cubit<int> { CounterCubit() : super(0);
void increment() => emit(state + 1);}
CounterBloc
Seção intitulada “CounterBloc”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
Seção intitulada “Vantagens do Bloc”Rastreabilidade
Seção intitulada “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
:
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.
Transformações Avançadas de Eventos
Seção intitulada “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
.
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.