Como Integrar Flutter Modular com Flutter Bloc (Cubit) de Forma Simples e Usando Estados Customizados (Classes)

Raphael Pontes
7 min readMay 3, 2024

--

ARTIGO EM INGLÊS

O mundo das libs do Flutter está em constante evolução. Mas, e se pudéssemos combinar o poder do Flutter Modular com a simplicidade do Flutter Bloc (Cubit)? Neste artigo, vamos explorar exatamente isso e desvendar os segredos dessa integração poderosa.

Flutter Modular

Flutter Modular é um pacote que simplifica a organização e a modularização de projetos Flutter. Ele oferece uma estrutura baseada em módulos. Além de proporcionar a injeção de dependencias e a navegação por rotas.

Cubit

Cubit é uma implementação do padrão de design Bloc (Business Logic Component) no Flutter, desenvolvido pela comunidade. Ele simplifica o gerenciamento de estado em aplicativos Flutter, tornando-o mais fácil de entender e manter. Se você quiser mergulhar mais fundo no Cubit, confira meu outro artigo aqui.

Instalando as libs

Este processo é essencial para garantir que você tenha acesso às funcionalidades do Cubit e do Modular no seu aplicativo.

flutter pub add flutter_modular flutter_bloc

Integração do Modular + Cubit

Para integrar o Flutter Modular com o Flutter Bloc (Cubit), adotaremos a estrutura de pastas padrão do Modular que promove uma separação clara dos modulos do projeto.

Nossa estrutura de pastas foi organizada da seguinte forma:

| lib
|— main.dart
|— app/
| — — app_module.dart
| — — app_widget.dart
| — — home/
| — — — home_cubit.dart
| — — — home_module.dart
| — — — home_page.dart

Agora para explicar melhor irei apresentar os trechos de código e explicar os pontos mais importantes.

// main.dart
void main() {
runApp(
ModularApp(
module: AppModule(),
child: const AppWidget(),
),
);
}
// app_module.dart
class AppModule extends Module {
@override
List<Module> get imports => const [];

@override
void binds(Injector i) {}

@override
void exportedBinds(Injector i) {}

@override
void routes(RouteManager r) {
r.module('/', module: HomeModule());
}
}

A versão do flutter_modular usada nesse artigo é a 6.3.3 e a forma de herdar os métodos da classe Module é dessa forma apresentada acima.

O Module é uma classe do pacote flutter_modular que ajuda na organização e modularização de projetos Flutter.

imports: retorna uma lista de módulos que este módulo importa. Neste caso, não há nenhum módulo importado ([]).

binds: é usado para definir as dependências (bindings) do módulo. Aqui, estamos deixando-o vazio, o que significa que não estamos definindo nenhuma dependência neste módulo específico.

exportedBinds: é usado para definir as dependências que serão exportadas para outros módulos. Assim como o binds, estamos deixando-o vazio neste exemplo.

routes: O método routes é usado para definir as rotas do módulo. Uma rota é uma URL que direciona para uma determinada parte do aplicativo. Aqui, estamos definindo a rota raiz (/) para apontar para o módulo HomeModule.

Em resumo, o arquivo app_module.dart está definindo o módulo principal do aplicativo (AppModule). Ele importa outros módulos, define dependências, exporta dependências e configura as rotas do aplicativo. Essas configurações são importantes para organizar e estruturar o projeto.

// app_widget.dart
class AppWidget extends StatelessWidget {
const AppWidget({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp.router(
theme: ThemeData.light(),
title: 'Modular Cubit Example',
routeInformationParser: Modular.routeInformationParser,
routerDelegate: Modular.routerDelegate,
);
}
}

o AppWidget configura e inicializa o aplicativo Flutter, fornecendo temas, títulos e configurações de roteamento personalizadas usando o MaterialApp.router. Isso é essencial para integrar o Flutter Modular com o Flutter Bloc (Cubit) e garantir uma experiência de usuário suave e consistente.

// home_module.dart
class HomeModule extends Module {
@override
List<Module> get imports => const [];

@override
void binds(Injector i) {}

@override
void exportedBinds(Injector i) {}

@override
void routes(RouteManager r) {
r.child(
'/',
child: (_) => BlocProvider(
create: (_) => HomeCubit(),
child: const HomePage(),
),
);
}
}

o HomeModule define as configurações específicas do módulo de Home do aplicativo, incluindo a definição das rotas e a configuração do cubit HomeCubit para a página inicial (HomePage).

Vale ressaltar que dentro de r.childexiste um BlocProvider ele está fornecendo o HomeCubit() para a árvore de widgets dessa rota. Isso é útil quando você precisa que uma classe cubit esteja disponível para os widgets dentro de uma determinada rota, mas não globalmente em todo o aplicativo.

No exemplo do home_module.dart, o BlocProvider está envolvendo a HomePage. Isso significa que qualquer widget dentro de HomePage que precisar acessar o HomeCubit terá acesso a ele.

// home_page.dart
class HomePage extends StatelessWidget {
const HomePage({super.key});

@override
Widget build(BuildContext context) {
final HomeCubit homeCubit = BlocProvider.of<HomeCubit>(context);

return Scaffold(
appBar: AppBar(
title: const Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<HomeCubit, CounterState>(
builder: (context, state) {
return Text('Counter: ${state.count}');
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FloatingActionButton(
onPressed: () => homeCubit.increment(),
child: const Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => homeCubit.decrement(),
child: const Icon(Icons.remove),
),
],
),
],
),
),
);
}
}

Dentro do método build, usamos BlocProvider.of<HomeCubit>(context) para obter a instância do HomeCubit fornecida pelo BlocProvider. Isso nos permite acessar os métodos do cubit para interagir com o estado.

BlocBuilder é usado para reconstruir seus filhos toda vez que o estado do cubit fornecido muda. Aqui, estamos exibindo o valor atual do contador no texto.

FloatingActionButton define dois botões flutuantes: um para incrementar o contador quando pressionado e outro para decrementar o contador, ambos chamando seus métodos respectivos implemetados no cubit.

// home_cubit.dart
class HomeCubit extends Cubit<CounterState> {
HomeCubit() : super(CounterInitialState());

void increment() {
emit(CounterIncrementedState(state.count + 1));
}

void decrement() {
emit(CounterDecrementedState(state.count - 1));
}
}

// states

abstract class CounterState {
final int count;

CounterState(this.count);
}

class CounterInitialState extends CounterState {
CounterInitialState() : super(0);
}

class CounterIncrementedState extends CounterState {
CounterIncrementedState(int count) : super(count);
}

class CounterDecrementedState extends CounterState {
CounterDecrementedState(int count) : super(count);
}

HomeCubit é uma classe que estende Cubit<CounterState>. Cubit é uma classe do pacote flutter_bloc que gerencia o estado e a lógica de negócios em um aplicativo Flutter. Aqui, HomeCubit gerencia o estado do contador.

O construtor HomeCubit() chama o construtor da classe pai Cubit<CounterState> e passa CounterInitialState() como o estado inicial do cubit. Isso significa que o estado inicial do contador é CounterInitialState.

increment() e decrement() são métodos usados para aumentar e diminuir o contador, respectivamente. Eles usam o método emit para emitir novos estados do contador com base no estado atual. Por exemplo, increment() emite um novo estado CounterIncrementedState com o contador incrementado em 1 em relação ao estado atual.

Os estados do contador são representados por classes que estendem CounterState, que é uma classe abstrata. Cada classe de estado contém um campo count, que representa o valor atual do contador.

CounterInitialState representa o estado inicial do contador, onde o contador começa com o valor 0 e sendo inicializada junta com o construtor conforme abaixo.

HomeCubit() : super(CounterInitialState());

CounterIncrementedState representa o estado do contador após a operação de incremento, onde o contador é aumentado em 1 em relação ao estado anterior.

CounterDecrementedState representa o estado do contador após a operação de decremento, onde o contador é diminuído em 1 em relação ao estado anterior.

O arquivo home_cubit.dart define a classe cubit HomeCubit responsável por gerenciar o estado do contador e os estados relacionados ao contador, incluindo o estado inicial, o estado após o incremento e o estado após o decremento. Ele fornece métodos para alterar o estado do contador de forma reativa e eficiente.

Usar classes para representar os estados como é feito no exemplo fornecido, traz várias vantagens e é considerado uma prática recomendada. Aqui estão algumas razões pelas quais é importante usar classes como estado:

  1. Clareza e organização: Permite organizar e estruturar o código de forma clara e compreensível. Cada estado é representado por uma classe separada, o que torna mais fácil entender e manter o código, especialmente em aplicativos com estados complexos.
  2. Encapsulamento de dados: As classes de estado encapsulam os dados relacionados a um determinado estado, tornando-os mais fáceis de gerenciar e manipular. Isso promove a coesão e reduz o acoplamento entre os diferentes componentes do aplicativo.
  3. Reutilização de código: Você pode reutilizar essas classes em diferentes partes do seu aplicativo, facilitando a consistência e a reutilização de código. Por exemplo, se você tiver um estado de contador em várias telas do seu aplicativo, poderá reutilizar a mesma classe de estado em todas elas.
  4. Tipagem estática: O uso de classes como estado promove a tipagem estática, o que ajuda a detectar erros de tipo em tempo de compilação. Isso proporciona mais segurança e robustez ao código, reduzindo a probabilidade de erros durante a execução.
  5. Facilidade de teste: As classes de estado são objetos simples e independentes que podem ser facilmente testados de forma isolada. Isso facilita a escrita de testes unitários para validar o comportamento do aplicativo em diferentes estados.
  6. Extensibilidade: Você pode facilmente adicionar novos estados e funcionalidades ao seu aplicativo, simplesmente criando novas classes de estado e atualizando a lógica de acordo. Isso torna o aplicativo mais flexível e adaptável a mudanças futuras.

Conclusão

Integrar o Flutter Modular com o Flutter Bloc (Cubit) pode parecer assustador à primeira vista, mas com a abordagem certa e as ferramentas certas, é uma tarefa alcançável. Espero que este artigo tenha fornecido insights valiosos sobre como realizar essa integração. Continue explorando e experimentando para aprimorar suas habilidades de desenvolvimento Flutter!

Para aprender mais sobre Flutter Modular, confira a documentação oficial aqui e para descobrir mais sobre o Cubit leia a documentação aqui ou meu outro artigo aqui.

Código fonte: https://github.com/rkpontes/flutter_modular_cubit

Te ajudei? Buy me a coffee

Follow me in

--

--

Raphael Pontes
Raphael Pontes

Written by Raphael Pontes

Software Engineer - Mobile Developer - Expert Flutter Developer - Follow Me medium/instagram: @raphaelkennedy

No responses yet