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