Sabia que o BLoC é muito mais do que só um package do Flutter? Pois é, ele também é um padrão poderoso que pode transformar a forma como você organiza seu código e resolve sua lógica de negócios. Vou te mostrar de um jeito simples o que é o BLoC, como ele funciona, como aplicar no seu projeto e, também, os prós e contras de usar esse padrão. Então, pega um cafézinho e vem comigo descobrir tudo sobre esse tal de BLoC! 🚀
Tópicos
- O que é o BLoC?
- Principais conceitos
- Como o BLoC funciona?
- Quais as vantagens e desvantagens?
- Conclusão
- Referências
O que é o BLoC?
O BLoC (Business Logic Component) não é apenas um package ou uma biblioteca, mas também um dos padrões de gerenciamento de estado mais populares no ecossistema Flutter. Ele ajuda a organizar nosso código ao separar a lógica de negócios da interface do usuário (UI), tornando nossa aplicação mais limpa, legível e fácil de escalar e, consequentemente, mais eficiente e manutenível.
Principais conceitos
Eventos: São ações ou intenções que vem da interface do usuário que indicam que algo deve ser feito. Por exemplo: ao preencher um formulário de cadastro, quando você aperta o botão de enviar os dados, o evento de registerUser
é disparado.
Estados: Os estados mostram como a lógica de negócios está em um dado momento e são enviados pelo BLoC. Ex: RegisterUserInitial
, RegisterUserLoading
, RegisterUserSuccess
. Você pode ver mais sobre estados no artigo Entendendo State Pattern
Sink: É o “porta de entrada” dos eventos. A UI manda os eventos pelo sink e o BLoC processa tudo.
Streams: As streams são como canais de comunicação entre a UI e o BLoC. Elas permitem que os eventos sejam enviados e os estados sejam recebidos de forma assíncrona.
BlocProvider: É responsável por fornecer e disponibilizar uma instância do BLoC para a árvore de widgets da aplicação, garantindo que eles tenham acesso à lógica e possam usá-la de forma fácil e organizada.
BlocBuilder: É o widget que atualiza a UI sempre que um novo estado chega, garantindo que a interface esteja sempre em sintonia com a lógica do aplicativo.
BlocListener: É o widget que fica de olho nas mudanças de estado e executa ações em resposta, como exibir um alerta ou iniciar uma animação, sem alterar a UI diretamente.
Como o BLoC funciona?
O BLoC trabalha como um mediador entre a interface do usuário e a lógica da nossa aplicação. O fluxo funciona assim:
Dispara o eventos: A interface envia ações (como cliques e interações) para o BLoC, transformando-as em eventos.
Processa os dados: O BLoC recebe esses eventos, executa a lógica necessária (validações, chamadas a API, etc) e decide o próximo passo.
Gera um novo estado: Após processar os dados, o BLoC gera novos estados que representam qual a condição atual da aplicação.
Atualiza a Interface: A UI escuta essas mudanças de estado e se ajusta automaticamente para refletir os novos dados ou comportamentos.
Tá mas como é isso na prática? Vamos lá.
Exemplo básico sem package
Primeiro, vamos ver como é o uso do BLoC puro, sem a utilização de package:
Classe de estado:
- É onde ficarão os estados do nosso contador, no caso só precisamos de um.
class CounterState {
final int counterValue;
CounterState(this.counterValue);
}
Classe de evento:
- É onde estarão os eventos referentes ao nosso contador que podem ser chamados pela tela, nesse caso o de incremento.
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
CounterBloc:
- Nessa classe, o BLoC é implementado usando Stream e Sink. Quando o botão na tela é pressionado, a função
increment()
é chamada. Ela adiciona o eventoIncrementEvent
no controlador de eventos (_eventController
). O BLoC escuta esses através da stream no construtor e chama a função_mapEventToState
. Essa função verifica qual é o evento. Se for umIncrementEvent
, o contador é aumentado e um novo estado, com o valor atualizado, é enviado para o controlador de estados (_stateController
), que é usado para mostrar o novo valor do contador na tela.
import 'dart:async';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterBloc {
final _stateController = StreamController<CounterState>(); // Controlador para o estado do contador.
final _eventController = StreamController<CounterEvent>(); // Controlador para eventos.
int _counter = 0; // Valor atual do contador.
CounterBloc() {
// Escuta os eventos e processa as mudanças de estado.
_eventController.stream.listen(_mapEventToState);
// Adiciona o estado inicial ao fluxo.
_stateController.add(CounterState(_counter));
}
// Saída do estado do contador.
Stream<CounterState> get state => _stateController.stream;
// Método para adicionar o evento de incremento.
void increment() {
_eventController.sink.add(IncrementEvent());
}
// Identifica o evento e atualiza o estado do contador.
void _mapEventToState(CounterEvent event) {
if (event is IncrementEvent) {
_counter++;
_stateController.add(CounterState(_counter));
}
}
// Fecha os controladores ao descartar a página para liberar recursos.
void dispose() {
_stateController.close();
_eventController.close();
}
}
HomePage:
- Essa é a nossa tela onde o evento de incremento do contador será chamado. Aqui usamos um StreamBuilder para observar as mudanças no estado do contador. Sempre que o estado é atualizado, o StreamBuilder reconstrói a interface para exibir o novo valor do contador.
import 'package:counter_sem_bloc/app/features/home/bloc/counter_state.dart';
import 'package:flutter/material.dart';
import '../bloc/counter_bloc.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final CounterBloc _counterBloc = CounterBloc(); // Instancia o BLoC.
@override
void dispose() {
_counterBloc.dispose(); // Libera recursos do BLoC ao descartar a página.
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Contador BLoC - Sem Package', style: TextStyle(color: Colors.white, fontSize: 16)),
),
body: Center(
child: StreamBuilder<CounterState>(
stream: _counterBloc.state, // Escuta as mudanças no estado do contador.
initialData: CounterState(0), // Valor inicial do contador.
builder: (context, snapshot) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Pressione o botão para incrementar o contador.',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
Text(
'${snapshot.data?.counterValue}', // Exibe o valor atual do contador.
style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
),
],
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _counterBloc.increment, // Chama o método para incrementar o contador.
tooltip: 'Incrementar',
child: const Icon(Icons.add),
),
);
}
}
Exemplo básico usando flutter_bloc
Agora vamos ver como o BLoC é implementado usando o package flutter_bloc:
As classes de evento e estado são praticamente iguais ao do exemplo acima:
Classe de estado:
- Como dito anteriormente, essa é a classe onde vai conter os estados do contador.
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int counterValue;
const CounterState(this.counterValue);
@override
List<Object> get props => [counterValue];
}
Nessa classe, também fazemos o uso de um outro package chamado equatable que no BLoC, é usado para comparar estados e eventos. Assim, o BLoC só muda algo na tela se o estado ou evento realmente for diferente, evitando atualizar sem necessidade.
O
equatable
serve para o Dart entender quando dois objetos são iguais olhando o que tem dentro deles (os valores). Sem ele, o Dart só compara se os objetos estão no mesmo lugar da memória. Por exemplo:
final user1 = User('Alice', 25);
final user2 = User('Alice', 25);
print(user1 == user2); // false, porque são objetos diferentes na memória.
// mas usando o equatable o resultado será [true], pois consultará
// também os valores dentro desses objetos
Classe de evento:
- Também da mesma forma do exemplo anterior, essa classe vai ter os eventos referentes ao contador que no nosso caso é o evento de incremento:
import 'package:equatable/equatable.dart';
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object> get props => [];
}
class IncrementCounterEvent extends CounterEvent {}
CounterBloc:
- Ao clicar no botão de incrementar, o evento
IncrementCounterEvent
é adicionado aoCounterBloc
, que escuta o evento e incrementa o valor do contador, assim oCounterBloc
emite um novo estado com o valor do contador incrementado e o widget é reconstruído exibindo o novo valor do contador
import 'package:counter_using_flutter_bloc/app/bloc/counter_event.dart';
import 'package:counter_using_flutter_bloc/app/bloc/counter_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {// Inicia o estado do contador com o valor 0
on<IncrementCounterEvent>((event, emit) { // Escuta o evento IncrementCounterEvent
emit(CounterState(state.counterValue + 1)); // Emite um novo estado com o valor do contador incrementado
});
}
}
E por último nossa HomePage que é onde o evento vai ser chamado e assim a tela vai atualizar o estado do contador através do BlocBuilder
:
import 'package:counter_using_flutter_bloc/app/bloc/counter_bloc.dart';
import 'package:counter_using_flutter_bloc/app/bloc/counter_event.dart';
import 'package:counter_using_flutter_bloc/app/bloc/counter_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title});
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title, style: Theme.of(context).textTheme.bodyLarge),
),
body: Center(
// Escuta o estado do CounterBloc e reconstrói o widget quando o estado muda
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Pressione o botão para incrementar o contador.',
),
Text(
'${state.counterValue}', // Exibe o valor do contador
style: Theme.of(context).textTheme.headlineMedium,
),
],
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Adiciona o evento IncrementCounterEvent ao CounterBloc
context.read<CounterBloc>().add(IncrementCounterEvent());
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Opa, mas precisamos de mais um detalhe! Temos que inserir a instancia do nosso bloc dentro da árvore de widgets para que possamos acessá-la na nossa HomePage. Como fazemos isso? É simples, vamos adicionar um BlocProvider
como "pai" da nossa HomePage
, assim poderemos obter a instância a partir de qualquer parte da tela.
import 'package:counter_using_flutter_bloc/app/bloc/counter_bloc.dart';
import 'package:counter_using_flutter_bloc/app/pages/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Exemplo de uso do Flutter Bloc',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: BlocProvider( // Adiciona a instância do CounterBloc ao widget da página
create: (context) => CounterBloc(),
child: const HomePage(title: 'Contador usando Flutter Bloc'),
),
);
}
}
Caso quiséssemos acessar a instância do bloc em qualquer parte da aplicação, colocaríamos o
BlocProvider
como "pai" doMaterialApp
.
Assim, ambos os exemplos funcionam da mesma forma, mas com abordagens diferentes. No primeiro, mostramos como o BLoC funciona por trás dos panos, implementando sua lógica de maneira pura, sem uso pacotes. Já no segundo, utilizamos o package flutter_bloc
, que abstrai boa parte da implementação, tornando o código mais simples, organizado e enxuto.
Quais as vantagens e desvantagens?
Vantagens
Separação de responsabilidades | Mantém a lógica de negócios separada da interface do usuário, deixando o código mais organizado e fácil de trabalhar. |
---|---|
Testabilidade | Isolar a lógica de negócios facilita a criação de testes unitários, garantindo a qualidade do código. |
Escalabilidade | Organiza o código de forma a suportar o crescimento do app sem se perder em complexidade. |
Padronização | Garante uma abordagem padronizada no desenvolvimento, o que é vantajoso para equipes grandes |
Reutilização de código | Permite usar a mesma lógica de negócios em diferentes partes do app ou até em outros projetos |
Desvantagens
Curva de aprendizado | Pode ser intimidador para iniciantes por usar conceitos como streams, sinks e gerenciamento de estados. |
---|---|
Complexidade desnecessária para apps simples | Em projetos pequenos, o uso do BLoC pode parecer exagerado, adicionando mais código e estrutura do que o necessário. |
Mais verboso | Comparado a outros métodos de gerenciamento de estado, como Provider ou setState , o BLoC pode exigir mais código boilerplate. |
"Ain mas num sei o que significa boilerplate" Então agora vai saber:
Boilerplate é um termo usado em desenvolvimento de software para se referir a códigos ou estruturas que precisam ser repetidos em vários lugares ou projetos, muitas vezes sem muitas alterações.
Conclusão
O BLoC é um padrão poderoso que promove o desacoplamento, a escalabilidade e a testabilidade nas aplicações Flutter. Apesar da sua implementação manual ser mais trabalhosa, o uso de pacotes como flutter_bloc
torna o processo bem mais simples e direto. Usando o BLoC, podemos ter uma interface do usuário mais reativa e alinhada com os princípios de boas práticas de desenvolvimento, permitindo que criemos aplicações mais robustas e fáceis de manter a longo prazo.
Espero que tenham gostado do artigo. Abaixo vou deixar alguns links de materiais relacionados que podem ajudar e complementar esse conteúdo, além do exemplo de implementação que usei. Qualquer dúvida é só chamar :3
Até a próxima.
Top comments (5)
Mto interessante como esses padrões derivados do redux se transformam nas outras tecnologias, arquitetura baseada em eventos eh genial! Parabéns pela ótima escrita, msm não sendo de flutter aprendi muito
Muito obrigada Cherry, que bom que gostou 💖
Interessante demais, esse artigo caiu na hora certa pra mim!
Olha ai que coisa boa, que bom que gostou 💜
Excelente conteúdo como sempre!