DEV Community

Cover image for BLoC - Mais que um package, um padrão
Adryanne Kelly
Adryanne Kelly

Posted on

BLoC - Mais que um package, um padrão

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?

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);
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 evento IncrementEvent 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 um IncrementEvent, 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
            ),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

CounterBloc:

  • Ao clicar no botão de incrementar, o evento IncrementCounterEvent é adicionado ao CounterBloc, que escuta o evento e incrementa o valor do contador, assim o CounterBloc 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
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
            ),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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'),
            ),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Caso quiséssemos acessar a instância do bloc em qualquer parte da aplicação, colocaríamos o BlocProvider como "pai" do MaterialApp.

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.

Referências

Top comments (5)

Collapse
 
cherryramatis profile image
Cherry Ramatis

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

Collapse
 
adryannekelly profile image
Adryanne Kelly

Muito obrigada Cherry, que bom que gostou 💖

Collapse
 
leonardorafaeldev profile image
Leonardo Rafael Dev

Interessante demais, esse artigo caiu na hora certa pra mim!

Collapse
 
adryannekelly profile image
Adryanne Kelly

Olha ai que coisa boa, que bom que gostou 💜

Collapse
 
redrodrigoc profile image
Rodrigo Castro

Excelente conteúdo como sempre!