🚀 Mason no Flutter: Escalando seu Boilerplate como um Senior de Verdade

Se você já trabalhou em um projeto Flutter grande, provavelmente já passou por isso:

  • Criar pasta de feature manualmente
  • Criar cubit, state, page, bindings
  • Configurar DI (flutter_getit)
  • Criar usecases, repositories…
  • Repetir isso dezenas de vezes

Isso não escala.

É aqui que entra o Mason — uma das ferramentas mais subestimadas do ecossistema Flutter.

Segue o repositório no github

https://github.com/brasizza/mason_artigo.git

🧱 O que é o Mason?

O Mason é um gerador de código baseado em templates (bricks).

Você define uma estrutura padrão (boilerplate) e depois simplesmente executa um comando para gerar tudo automaticamente.

Pense nele como:

“um flutter create, só que para qualquer coisa que você quiser”


⚙️ Instalando o Mason

Instalação global via Dart

dart pub global activate mason_cli
mason --version


📦 Inicializando o Mason no projeto


Dentro da raiz do seu projeto Flutter:

mason init

Isso vai criar alguns arquivos de inicialização do mason (mason.yaml)


🧱 Criando sua primeira brick


Agora vamos criar uma brick para features:

mason new feature_getit

Isso vai gerar:


feature_getit/
├── brick/
└── brick.yaml

Dentro de __brick__ você define o template:

__brick__/
└── lib/
    └── src/
        └── features/
            └── {{name}}/
                ├── {{name}}_module.dart
                ├── domain/
                   ├── entities/
                      └── {{name}}.dart
                   └── repositories/
                       └── {{name}}_repository.dart
                ├── infra/
                   └── {{name}}_repository_impl.dart
                └── presentation/
                    ├── {{name}}_page.dart
                    └── cubit/
                        ├── {{name}}_cubit.dart
                        └── {{name}}_state.dart

🔥 Variáveis e transformação de nomes (ESSENCIAL)

O Mason placehoders ou anotações para os templates.

Você pode transformar variáveis automaticamente:

TipoExemplo
raw{{name}} → splash
PascalCase{{name.pascalCase()}} → Splash
camelCase{{name.camelCase()}} → splash
snake_case{{name.snakeCase()}} → splash

💡 Alguns exemplos de arquivos do brick

{{name}}_module.dart

import 'package:flutter_getit/flutter_getit.dart';
import 'domain/repositories/{{name}}_repository.dart';
import 'infra/{{name}}_repository_impl.dart';
import 'presentation/cubit/{{name}}_cubit.dart';
import 'presentation/{{name}}_page.dart';

class {{name.pascalCase()}}Module extends FlutterGetItModule {
  @override

  String get moduleRouteName => '/{{name}}';

  @override 

  List<Bind<Object>> get bindings => [

    Bind.lazySingleton<{{name.pascalCase()}}Repository>(
      (i) => {{name.pascalCase()}}RepositoryImpl(dio: i()),
    ),

    Bind.singleton<{{name.pascalCase()}}Cubit>(
      (i) => {{name.pascalCase()}}Cubit(repository: i()),
    ),  

  ];

  @override

  List<FlutterGetItPageRouter> get pages => [
      FlutterGetItPageRouter(
        name: '/',
        builder: (_) => const {{name.pascalCase()}}Page(),
      ),
  ];

}

splash_module.dart

import 'package:flutter_getit/flutter_getit.dart';
import 'domain/repositories/splash_repository.dart';
import 'infra/splash_repository_impl.dart';
import 'presentation/cubit/splash_cubit.dart';
import 'presentation/splash_page.dart';

class SplashModule extends FlutterGetItModule {
  @override
  String get moduleRouteName => '/splash';

  @override 
  List<Bind<Object>> get bindings => [

    Bind.lazySingleton<SplashRepository>(
      (i) => SplashRepositoryImpl(dio: i()),
    ),

    Bind.singleton<SplashCubit>(
      (i) => SplashCubit(repository: i()),
    ),  

  ];

  @override
  List<FlutterGetItPageRouter> get pages => [
      FlutterGetItPageRouter(
        name: '/',
        builder: (_) => const SplashPage(),
      ),
  ];



}

{{name}}_state.dart

part of '{{name}}_cubit.dart';
sealed class {{name.pascalCase()}}State {}

final class {{name.pascalCase()}}Initial extends {{name.pascalCase()}}State {}
final class {{name.pascalCase()}}Loading extends {{name.pascalCase()}}State {}
final class {{name.pascalCase()}}Loaded extends {{name.pascalCase()}}State {}
final class {{name.pascalCase()}}Error extends {{name.pascalCase()}}State {}

splash_state.dart

part of 'splash_cubit.dart';

sealed class SplashState {}

final class SplashInitial extends SplashState {}
final class SplashLoading extends SplashState {}
final class SplashLoaded extends SplashState {}
final class SplashError extends SplashState {}

Esse arquivo define os estados da feature.

Esse é um dos pontos mais acertados da brick

Porque praticamente toda feature assíncrona começa com esse ciclo:

  • Initial
  • Loading
  • Loaded
  • Error

Isso cobre boa parte dos fluxos reais.

Por que isso escala bem?

Porque você evita que cada dev invente uma convenção diferente.

Sem brick, um cria:

  • Idle
  • Fetching
  • Success

Outro cria:

  • Initial
  • Busy
  • Done

Outro nem separa estados.

Com a brick, todo mundo começa igual.

{{name}}_repository_impl.dart

import 'package:dio/dio.dart';
import '../domain/repositories/{{name}}_repository.dart';

class {{name.pascalCase()}}RepositoryImpl implements {{name.pascalCase()}}Repository {

  final Dio _dio;

  {{name.pascalCase()}}RepositoryImpl({required Dio dio}) : _dio = dio;

}

splash_repository_impl.dart

import 'package:dio/dio.dart';
import '../domain/repositories/splash_repository.dart';

class SplashRepositoryImpl implements SplashRepository {

  final Dio _dio;

  SplashRepositoryImpl({required Dio dio}) : _dio = dio;

}

Esse arquivo é a implementação concreta do contrato do repository.

Papel desse arquivo

Ele fica na camada de infra porque é aqui que normalmente vivem:

  • chamadas HTTP
  • acesso a banco local
  • adaptação de payload
  • mapeamento DTO → entity
  • integração com APIs externas

O que sua brick já deixa pronto?

Ela já injeta um Dio, então o repository nasce preparado para acesso HTTP.

Isso é muito bom em projeto real, porque a maioria das features acaba precisando de comunicação externa.

Leitura arquitetural

Esse arquivo representa a parte “que conversa com o mundo”.

Enquanto o domain/repositories define o que precisa ser feito, o infra/…_impl.dart define como será feito.

⚠️ Problema que você resolve com Mason

Sem Mason:

  • Código inconsistente
  • Devs criando estrutura diferente
  • Perda de tempo absurda
  • Bugs de DI

Com Mason:

  • Padronização total
  • Escalabilidade real
  • Onboarding rápido
  • Código previsível



⚡ Script para criação ultra rápida de features

Agora que você ja tem uma pequena noçao de como o mason pode facilitar sua vida , podemos criar um script simples e ser chamado ./make_feature splash e ele gera a feature completa.

#!/bin/bash

DESTINO_LIB="lib/src/features"
MASON_COMMAND="mason make feature_getit"

if [[ ! -f "pubspec.yaml" || ! -d "lib" ]]; then
  echo "❌ Execute na raiz do projeto."
  exit 1
fi

if [ -z "$1" ]; then
  echo "❌ Informe o nome da feature."
  echo "Ex: ./make_feature.sh empresa"
  exit 1
fi

FEATURE_NAME=$1

echo "🚀 Criando feature: $FEATURE_NAME"

mkdir -p "$DESTINO_LIB"

$MASON_COMMAND --name "$FEATURE_NAME"

if [ -d "$FEATURE_NAME" ]; then
  mv "$FEATURE_NAME" "$DESTINO_LIB/$FEATURE_NAME"
  echo "✅ Feature criada em: $DESTINO_LIB/$FEATURE_NAME"
else
  echo "⚠️ Verifique se a brick gerou a pasta corretamente."
fi

O Mason não é só uma ferramenta.

É um multiplicador de produtividade.

Se você trabalha com Flutter em projetos grandes, não usar Mason é:

aceitar perda de tempo todos os dias