Gerência de estado BloC e Cubit no Flutter
Olá pessoal, seguindo o assunto abordado no artigo Flutter Gerencia de Estado Nativa, agora iremos explorar a gerência de estados o BLoC, resumidamente veremos implementação e características distintas entre BloC e Cubit .
O Padrão de Lógica de Negócios do Componente (Business Logic of Component) ou BLoC para os íntimos é uma abordagem introduzida pelo Google durante a DartConf 2018.
O BLoC é conhecido por sua robustez, flexibilidade e escalabilidade, sendo amplamente utilizado.
E se tornou uma escolha popular entre os desenvolvedores Flutter, sendo uma vantagem a qualquer desenvolvedor aprimorar suas habilidades com a utilização desse cara.
O Bloc foi desenvolvido com três valores fundamentais :
- Simples: fácil de entender e pode ser usado por desenvolvedores com diversos níveis de habilidade.
- Poderoso: ajude a criar aplicativos incríveis e complexos compondo-os com componentes menores.
- Testável: teste facilmente todos os aspectos de um aplicativo para que possamos iterar com confiança.
No geral, tenta tornar as mudanças de estado previsíveis, regulando quando uma mudança de estado pode ocorrer e impondo uma única maneira de alterar o estado em todo o aplicativo.
Padrão BloC (Business Logic Component)
O padrão BLoC é uma arquitetura reativa que separa a lógica de negócios da camada de apresentação.
Ele utiliza streams para transmitir dados de um componente para outro, facilitando a comunicação entre diferentes partes do aplicativo.
Com o BLoC as operações assíncronas são tratadas de maneira simples e organizada, proporcionando uma experiência de desenvolvimento mais fluida
Particularidades e Uso
A escolha entre BLoC e Cubit depende das necessidades específicas do projeto e da complexidade do aplicativo.
Enquanto o BLoC oferece maior flexibilidade e capacidade de lidar com casos de uso mais complexos, o Cubit é uma opção mais simples e direta para situações menos exigentes.
Ambos são amplamente suportados pela comunidade Flutter que oferecem uma base sólida
Ao escolher entre eles, é importante considerar as necessidades específicas do projeto e o nível de complexidade envolvido, garantindo assim uma experiência de desenvolvimento eficiente e sem complicações.
Conceitos de BloC
Existem alguns conceitos básicos essenciais para entender como usar o pacote BLoC. Nas próximas seções, vamos discutir cada um deles detalhadamente e ver como eles se aplicam a um aplicativo contador.
Eventos
Eventos são a entrada para um bloc. Eles são geralmente adicionados em resposta a interações do usuário, como pressionar botões ou eventos do ciclo de vida, como carregamentos de páginas.
Programação assíncrona: Streams
Objetivo?
Streams fornecem uma sequência assíncrona de dados.
As sequências de dados incluem eventos gerados pelo usuário e dados lidos de arquivos. Você pode processar um stream usando await for ou listen() from the Stream API.
Streams fornecem uma maneira de responder a erros.
Existem dois tipos de streams: assinatura única ou transmissão.
A programação assíncrona no Dart é caracterizada pelas classes Future e Stream.
Um Future representa um cálculo que não é concluído imediatamente. Onde uma função normal retorna o resultado, uma função assíncrona retorna um Future, que eventualmente conterá o resultado. O futuro dirá quando o resultado estiver pronto.
Um stream é uma sequência de eventos assíncronos.
É como um Iterable assíncrono — onde, em vez de obter o próximo evento quando você o solicita, o fluxo informa que há um evento quando ele estiver pronto.
Recebendo eventos de stream
Os fluxos podem ser criados de várias maneiras, o que é um tópico para outro artigo, mas todos podem ser usados da mesma maneira: o loop for assíncrono (geralmente chamado apenas de await for) itera sobre os eventos de um fluxo como o loop for itera sobre um Iterable.
exemplo:
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
sum += value;
}
return sum;
}
Este código simplesmente captura cada evento de um fluxo de eventos inteiros, os soma e retorna (um futuro de) a soma. Quando o corpo do loop finaliza, a função entra em pausa até que o próximo evento chegue ou o fluxo termine.
A função é anotada com a palavra-chave async, requerida ao usar o loop await for.
Veja um exemplo do código anterior gerando um fluxo simples de números inteiros usando uma função async*.
void main() async {
var stream = countStream(2);
var sum = await sumStream(stream);
print(sum); // 3
}
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
sum += value;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
Execute o código acima em um projeto dart ou. DartPad
Os streams em Dart desempenham um papel crucial na transmissão tanto de dados quanto de erros de forma assíncrona.
Usamos um loop await for para processar eventos de um stream, o fluxo de execução pode ser interrompido devido ao término do stream ou à ocorrência de um erro.
Nesse contexto, a detecção e o tratamento de erros são fundamentais, sendo possível capturar e gerenciar esses erros utilizando a estrutura try-catch.
Assim, os streams fornecem uma maneira robusta de lidar com dados assíncronos e situações de erro em aplicativos Dart.
O código a seguir gera um erro quando o iterador do loop é igual a 4:
// Intentional exception
void main() async {
var stream = countStream(10);
final sum = await sumStream(stream);
if (sum == -1) {
print('Error in main call countStream ..........');
}
if (sum != -1) print(sum);
}
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
try {
await for (final value in stream) {
sum += value;
}
} catch (e) {
return -1;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 4) {
throw Exception('Intentional exception in countStream');
} else {
yield i;
}
}
}
Execute o código acima em um projeto dart ou. DartPad
Cubit
O Cubit é uma alternativa mais simplificada ao BLoC, introduzida como parte do pacote Flutter Bloc. Ele segue os mesmos princípios do BLoC, mas com uma API mais leve e direta.
O Cubit é uma classe que se estende BlocBasee pode ser estendida para gerenciar qualquer tipo de estado, e não possui dependências externas, tornando-o mais fácil de entender e implementar.
Ele é ideal para casos em que a complexidade oferecendo uma solução mais enxuta e focada em funcionalidades básicas de gerenciamento de estado.
Imagem 01 UI dispara uma função/método para um cubit que faz um pedido (request) e recebe uma resposta (response) então cubit por meio do emit emite um estado (state) para UI.
Os estados são a saída de um Cubit e representando uma parte do estado em um aplicativo.
Os componentes da UI podem ser notificados sobre estados e redesenhar partes deles com base no estado atual.
Ao criar um Cubit, é necessário especificar o tipo de estado que ele irá gerenciar.
No exemplo de CounterCubit mencionado acima, o estado pode ser representado por um número inteiro (int), como no código abaixo, porém, em situações mais complexas, pode ser necessário utilizar uma classe em vez de um tipo primitivo.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
}
A segunda coisa que precisamos fazer ao criar Cubit é especificar o estado inicial.
No trecho acima, estamos definindo o estado inicial como 0 internamente, mas também podemos permitir que Cubit seja mais flexível aceitando um valor externo por meio de seu construtor .
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}
Assim podemos instanciar CounterCubit com diferentes estados iniciais, como
final cubitA = CounterCubit(0); // state starts at 0
final cubitB = CounterCubit(10); // state starts at 10
Geramos novos estado implementando um método usando o emit como a seguir podemos ver increment:
void increment() => emit(state + 1);
Tratamento de erros Cubit
Cada um Cubit possui um addError método que pode ser usado para indicar que ocorreu um erro.
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
Dentro de um Cubit, é possível substituir o método onError para lidar com todos os erros relacionados a esse Cubit.
Essa abordagem permite centralizar o tratamento de erros dentro do Cubit, proporcionando uma maneira organizada de gerenciar situações de erro em toda a lógica de negócios associada.
onError também pode ser substituído por BlocObserver lidar com todos os erros relatados globalmente não falaremos sobre essa abordagem fica a critério de sua curiosidade ver mais sobre ele .
Nosso Cubit na íntegra:
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);
}
}
Exemplo simples implementação em dart:
void main(){
final cubit = CounterCubit();
cubit.increment();
}
Para rodar o código localmente crie um projeto dart com dart create my_program_cubit depois adicione o bloc com dart pub add bloc ..
BloC
O Bloc é uma classe avançada que se baseia em eventos para acionar mudanças de estado, em vez de depender de funções.
Além disso, Bloc estende BlocBase, o que significa que possui uma API pública semelhante ao Cubit.
No entanto, em vez de chamar diretamente a função emit para emitir um novo estado, Bloc recebe eventos e converte esses eventos de entrada em estados de saída.
Imagem 02 UI dispara um event para um bloc que faz um pedido (request) e recebe uma resposta (response) um novo estado (state) e emitido para UI.
Definições de classes:
sealed class CounterEvent {}
final class CounterIncrementEvent extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
}
Como ao criar o CounterCubit, devemos especificar um estado inicial passando-o para a superclasse via super
Mudanças no estado do bloc
No Bloc, é necessário registrar manipuladores de eventos usando a API on<Event>, ao contrário do Cubit, que utiliza funções. Um manipulador de eventos tem a responsabilidade de converter os eventos recebidos em zero ou mais estados de saída.
Iniciamos o on<Event>, no corpo do construtor
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementEvent>((event, emit) {
// handle incoming `CounterIncrementEvent` event
});
}
}
Para deixar mais organizado criamos um método _onIncrement como a seguir.
FutureOr<void> _onIncrement(CounterIncrementEvent event, Emitter<int> emit) {
// handle incoming `CounterIncrementEvent` event
emit(state + 1);
}
O _onIncrement recebe dois parâmetros event evento do tipo CounterIncrementEvent e um emitter do tipo int o nosso estado (state) e por meio do emit ele é transmitido
Agora chamamos no método _onIncrement no on<Event> de uma forma mais elegante.
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementEvent>(_onIncrement);
}
No trecho mencionado, estamos registrando um manipulador de eventos EventHandler para lidar com todos os eventos CounterIncrementEvent. Para cada evento CounterIncrementEvent recebido, podemos acessar o estado atual do bloco por meio do getter state e emitir (state + 1).
Em nosso exemplo usamos um int como estado de saída devida simplicidade do código em implementações mais complexa poderíamos ter várias classes uma para cada estado algo como o exemplo abaixo .
sealed class SearchZipCodeState {}
final class SearchZipCodeInit implements SearchZipCodeState {}
final class SearchZipCodeSuccess implements SearchZipCodeState {
final Map data;
SearchZipCodeSucces(this.data);
}
final class SearchZipCodeLoading implements SearchZipCodeState {}
final class SearchZipCodeError implements SearchZipCodeState {
final String message;
const SearchZipCodeError(this.message);
}
Nesse exemplo, o estado é implementado como classe onde SearchZipCodeState representa o estado de uma pesquisa por cep.
Usando um BloC
Exemplo de uso Básico
Future<void> main() async {
final bloc = CounterBloc();
print('Init State ${bloc.state}');
bloc.add(CounterIncrementEvent());
await Future.delayed(Duration.zero);
print('Current State ${bloc.state}');
await bloc.close();
}
No código acima, instanciamos o CounterBloc, imprimindo cada mudança de estado. Em seguida, adicionamos o evento CounterIncrementEvent, que ativa o EventHandler on<CounterIncrementEvent> e emite um novo estado. Por fim, cancelamos a inscrição quando não desejamos mais receber atualizações e fechamos o Bloc.
Uso de fluxo
Future<void> main() async {
final bloc = CounterBloc();
final subscription = bloc.stream.listen(print); // 1
bloc.add(CounterIncrementEvent());
await Future.delayed(Duration.zero);
await subscription.cancel();
await bloc.close();
}
Acima, estamos assinando CounterBloc e chamando print em cada mudança de estado. Estamos então adicionando o CounterIncrementEvent que aciona on<CounterIncrementEvent> EventHandlere emite um novo estado. Por fim, cancela a assinatura quando não receber atualizações e fechando o Bloc.
Observando um BloC
O Observer estende de BlocBase, podemos observar todas as mudanças de estado para um Bloc using onChange.
Este tópico fica a critério, interesse e curiosidade do leitor pesquisar por ele.
O Bloc é uma ferramenta poderosa, porém, sua implementação pode não ser tão fácil para iniciantes. Compreender seu funcionamento requer estudo de teoria e prática.
Revisão:
Exploramos a gerência de estado no Flutter utilizando o Bloc e o Cubit. Começa destacando a importância do padrão BLoC (Business Logic Component), conhecido por sua robustez, flexibilidade e escalabilidade.
Em seguida, discutimos as características distintas entre Bloc e Cubit, apresentando o Bloc como uma abordagem mais avançada e o Cubit como uma alternativa simplificada. Destaca que a escolha entre eles depende das necessidades do projeto e da complexidade envolvida.
Explora o funcionamento básico do Bloc e do Cubit, destacando a definição de manipuladores de eventos, a emissão de estados e o tratamento de erros. Além disso, apresenta exemplos de implementação e uso tanto para o Bloc quanto para o Cubit.
Finalmente, ressalta a importância de compreender essas ferramentas poderosas para a gestão de estado no Flutter, destacando que sua implementação pode ser desafiadora para iniciantes, requerendo estudo e prática dedicados.
“Não existe uma solução perfeita de gerenciamento de estado!
O importante é que você escolha aquele que funciona melhor para sua equipe e seu projeto”.
vlw ;)
Referências:
https://bloclibrary.dev/getting-started/
https://medium.com/@ddanielddiniz/bloc-vs-cubit-flutter-cfabe13040d4