2195 words
11 minutes
Automate Your Flutter Development with Code Generation

The Problem We All Face#

Picture this: You’re building a Flutter app, and you need to add a new feature. You create the folder structure, add the data layer, domain layer, presentation layer, set up dependency injection, configure routing, add localization files, register everything in your app’s main files… and 45 minutes later, you haven’t written a single line of business logic yet.

Sound familiar?

This repetitive process isn’t just time-consuming—it’s error-prone. Forget to register a route? Your navigation breaks. Typo in an import? Build fails. Different team members structure their features differently? Your codebase becomes inconsistent.

There’s a better way.

Enter Mason#

Mason is a code generation tool for Dart and Flutter projects, created by Felix Angelov (the same developer behind the BLoC pattern library). Think of it as “templates on steroids”—you define reusable code templates (called “bricks”), and Mason generates all the boilerplate code for you, complete with custom logic and automation.

But here’s where it gets interesting: Mason doesn’t just copy-paste files. It can:

  • Accept user input and transform it (snake_case to PascalCase, etc.)
  • Run custom Dart code before and after generation
  • Modify existing files to wire up your generated code
  • Execute shell commands like running build tools

In this tutorial, I’ll show you how to leverage Mason using real-world examples from a production Flutter app. By the end, you’ll know how to create your own bricks that can save your team hours of work every week.

Getting Started with Mason#

Installation#

First, install Mason CLI globally:

Terminal window
dart pub global activate mason_cli

Verify the installation:

Terminal window
mason --version

Your First Brick#

Let’s understand Mason’s basic concepts by looking at how it works. A Mason project has two key files:

mason.yaml - Your project’s brick registry:

bricks:
feature:
path: ./bricks/feature/
component:
path: ./bricks/component/

This file tells Mason which bricks are available in your project.

Basic Concepts#

Before we dive into complex examples, let’s clarify the terminology:

  • Brick: A template package that generates code
  • Template: The actual files and folders with placeholders
  • Variables: User inputs that get substituted into templates
  • Hooks: Dart scripts that run before/after generation
  • Mustache syntax: The {{variable}} placeholder format

A Simple Example#

Let’s create a basic brick to understand the flow:

Terminal window
mason new hello

This creates a brick structure:

bricks/hello/
├── brick.yaml
├── __brick__/
│ └── hello.txt
└── hooks/

The brick.yaml defines your brick:

name: hello
description: A simple hello brick
version: 0.1.0
vars:
name:
type: string
prompt: What is your name?

The template file (__brick__/hello.txt) uses your variable:

Hello, {{name}}!
Welcome to Mason.

Run it:

Terminal window
mason make hello
# Prompt: What is your name? → John

Output (hello.txt):

Hello, John!
Welcome to Mason.

Simple, right? Now let’s see what’s possible with real-world examples.

Real-World Example: Feature Module Generation#

Let me show you a brick from a production Flutter app that generates complete feature modules. This is where Mason’s power really shines.

The Challenge#

In a well-architected Flutter app using clean architecture, creating a new feature requires:

  1. Creating a package structure with multiple folders
  2. Setting up localization files (potentially multiple languages)
  3. Configuring dependency injection
  4. Setting up routing
  5. Registering the feature in the main app’s:
    • pubspec.yaml
    • Localization delegates
    • Router configuration
    • Dependency injection container
  6. Running build tools

Doing this manually takes 30-45 minutes and is prone to errors. With Mason? Less than 30 seconds.

The Brick Structure#

Here’s the brick configuration (bricks/feature/brick.yaml):

name: feature
description: Generate a complete feature module
version: 0.1.0+1
environment:
mason: ^0.1.1
vars:
feature:
type: string
description: Your feature name
prompt: What is the feature name? (in snake_case)

The template structure (bricks/feature/__brick__/):

packages/feature/{{feature}}/
├── l10n.yaml
├── pubspec.yaml
└── lib/
├── {{feature}}_feature_routes.dart
├── data/
├── domain/
│ ├── model/
│ ├── repository/
│ └── usecase/
├── di/
│ └── {{feature}}_feature_di.dart
├── l10n/
│ ├── {{feature}}_ar.arb
│ ├── {{feature}}_en.arb
│ └── {{feature}}_fr.arb
├── page/
└── ui/

Notice the {{feature}} placeholders in file and folder names? Mason will replace these with the user’s input.

Template Files with Logic#

Here’s a template file ({{feature}}_feature_di.dart):

import 'package:get_it/get_it.dart';
void register{{feature.pascalCase()}}FeatureDependencies(GetIt locator) {
// register dependencies here
}

The {{feature.pascalCase()}} function transforms the input. If the user enters user_profile, the output becomes:

import 'package:get_it/get_it.dart';
void registerUserProfileFeatureDependencies(GetIt locator) {
// register dependencies here
}

Mason includes built-in transformations:

  • {{variable.camelCase()}} → userProfile
  • {{variable.pascalCase()}} → UserProfile
  • {{variable.snakeCase()}} → user_profile
  • {{variable.constantCase()}} → USER_PROFILE
  • And more!

Running the Brick#

Terminal window
mason make feature
? What is the feature name? (in snake_case) notifications

Mason generates the entire structure in seconds. But we’re just getting started…

Advanced: Post-Generation Hooks#

Here’s where Mason becomes truly powerful. After generating files, we need to integrate them into the app. Instead of manual steps, we can automate everything with post-generation hooks.

What Are Hooks?#

Hooks are Dart scripts that run before (pre_gen.dart) or after (post_gen.dart) generation. They have full access to:

  • The file system
  • User’s input variables
  • Shell commands
  • Dart packages

A Production Hook Example#

Let’s look at the actual post-generation hook from the feature brick (bricks/feature/hooks/post_gen.dart):

import 'dart:io';
import 'package:recase/recase.dart';
import 'package:mason/mason.dart';
void run(HookContext context) async {
final feature = context.vars['feature'];
_insertFeatureInAppPubspec(feature);
_insertFeatureInAppLocalizations(feature);
_insertFeatureInAppFeatureRoutes(feature);
_insertFeatureInAppServiceLocator(feature);
await Process.run('bash', ['-c', 'melos bs']);
}

This hook automatically:

  1. Adds the feature to pubspec.yaml
  2. Registers localization delegates
  3. Registers routes
  4. Registers dependency injection
  5. Runs the monorepo bootstrap command

The developer never touches these files manually.

The Insertion Pattern#

Here’s a clever pattern used in the hook for adding code to existing files:

void _insertFeatureInAppPubspec(feature) {
final file = File('apps/my_app/pubspec.yaml');
final lines = file.readAsLinesSync();
_insertBeforeAnchor('#endregion Features', " feature_$feature:", lines);
file.writeAsStringSync(lines.join('\n'));
}
void _insertBeforeAnchor(String anchor, String insert, List<String> lines) {
final index = lines.indexWhere((line) => line.contains(anchor));
lines.insert(index, insert);
}

The pubspec.yaml file has “anchor” comments:

dependencies:
#region Features
feature_authentication:
feature_home:
#endregion Features

The hook finds the #endregion Features marker and inserts the new feature before it. This keeps the code organized and makes automation reliable.

Registering Localizations Automatically#

Another example from the same hook:

void _insertFeatureInAppLocalizations(feature) {
final file = File('apps/my_app/lib/l10n/max_it_localizations.dart');
final lines = file.readAsLinesSync();
// Add import at the top
lines.insert(0,
"import 'package:feature_$feature/l10n/${feature}_localizations.dart';");
// Add delegate
final featurePascalCase = ReCase(feature).pascalCase;
_insertBeforeAnchor(
'// #endregion Features Delegates',
" ${featurePascalCase}Localizations.delegate,",
lines,
);
// Add supported locales
_insertBeforeAnchor(
'// #endregion Features Supported Locales',
" ...${featurePascalCase}Localizations.supportedLocales,",
lines,
);
file.writeAsStringSync(lines.join('\n'));
}

This single function:

  • Adds the import statement
  • Registers the localization delegate
  • Adds the supported locales

All without the developer lifting a finger.

Running Shell Commands#

The hook can also execute commands:

await Process.run('bash', ['-c', 'melos bs']);
// Or run build_runner for code generation
final generateCommand = 'dart run build_runner build --delete-conflicting-outputs';
await Process.run('bash', [
'-c',
"melos exec --depends-on build_runner --scope feature_$feature -- $generateCommand",
]);

This ensures all build steps complete automatically after generation.

Hook Dependencies#

Hooks can use Dart packages. Define them in bricks/feature/hooks/pubspec.yaml:

name: feature_hooks
environment:
sdk: ^3.5.4
dependencies:
mason: ^0.1.1
recase: ^4.1.0

Now the recase package is available for string transformations in your hook.

Real-World Example 2: Page Generation with BLoC#

Let’s look at another brick from the same project that generates pages within features using the BLoC pattern.

The Brick Configuration#

name: page
description: Generate a BLoC-based page
version: 0.1.0+1
vars:
feature:
type: enum
prompt: What is the feature name?
values:
- home
- authentication
- notifications
- profile
page:
type: string
prompt: What is the page name? (in snake_case)
addFeatureRoute:
type: boolean
default: false
prompt: Expose the page as a feature route?

Key differences from the feature brick:

  1. Enum Variable: The feature variable is an enum, so users select from existing features
  2. Boolean Variable: Conditional logic based on whether this is an app-level route
  3. Multiple Variables: Three inputs that work together

The Generated Structure#

packages/feature/{{feature}}/lib/page/{{page}}/
├── {{page}}_bloc.dart
├── {{page}}_route.dart
├── {{page}}_screen.dart
├── mapper/
└── model/
├── {{page}}_event.dart
└── {{page}}_state.dart

Template Example: BLoC Boilerplate#

Here’s what the {{page}}_screen.dart template might look like:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '{{page}}_bloc.dart';
import 'model/{{page}}_state.dart';
import 'model/{{page}}_event.dart';
class {{page.pascalCase()}}Screen extends StatelessWidget {
const {{page.pascalCase()}}Screen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => {{page.pascalCase()}}Bloc(),
child: const _{{page.pascalCase()}}View(),
);
}
}
class _{{page.pascalCase()}}View extends StatelessWidget {
const _{{page.pascalCase()}}View();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('{{page.titleCase()}}'),
),
body: BlocBuilder<{{page.pascalCase()}}Bloc, {{page.pascalCase()}}State>(
builder: (context, state) {
return const Center(
child: Text('{{page.pascalCase()}} Screen'),
);
},
),
);
}
}

If the user enters user_settings, this generates a complete BLoC screen with proper naming.

Conditional Hook Logic#

The page brick’s hook uses conditional logic:

void run(HookContext context) async {
final feature = context.vars['feature'];
final page = context.vars['page'];
final addFeatureRoute = context.vars['addFeatureRoute'] as bool;
_insertScreenInFeatureRoutes(feature, page);
if (addFeatureRoute) {
_insertScreenInAppRoutes(feature, page);
}
_triggerBuildRunner(feature);
}

If addFeatureRoute is true, it performs additional integration. Otherwise, it skips that step.

Updating the Feature Brick Dynamically#

Here’s something clever—the feature brick’s hook automatically updates the page brick:

void _insertFeatureInPageBrick(feature) {
final file = File('bricks/page/brick.yaml');
final lines = file.readAsLinesSync();
_insertBeforeAnchor('#endregion Feature list', " - $feature", lines);
file.writeAsStringSync(lines.join('\n'));
}

When you create a new feature, it automatically appears in the page brick’s feature dropdown. The bricks evolve with your project.

Best Practices & Patterns#

Here are some best practices we’ve identified based on the challenges we encountered in our project. These patterns help make our bricks more maintainable and consistent.

1. Use Region Markers for Insertion Points#

Instead of fragile line number-based insertion, use comments as anchors:

dependencies:
#region Features
feature_home:
feature_auth:
#endregion Features

Your hook looks for #endregion Features and inserts before it. This is:

  • Reliable (doesn’t break if other code changes)
  • Self-documenting (developers see the organization)
  • IDE-friendly (many editors support region folding)

2. Validate with Pre-Generation Hooks#

Use pre_gen.dart to check preconditions:

void run(HookContext context) {
if (!File('pubspec.yaml').existsSync()) {
throw Exception('Must run this brick from project root.');
}
final feature = context.vars['feature'];
if (Directory('packages/feature/$feature').existsSync()) {
throw Exception('Feature $feature already exists!');
}
}

This prevents mistakes before generation starts.

3. Keep Templates Simple, Logic in Hooks#

Don’t overcomplicate templates with conditional logic. Keep templates clean and handle complexity in hooks.

Good:

// Template is clean
class {{name.pascalCase()}}Service {}
// Hook handles registration
void _registerInDI(name) {
// complex registration logic
}

Bad:

// Too much logic in template
{{#if needsAuth}}
{{#if isAdmin}}
// complex nested conditionals
{{/if}}
{{/if}}

4. Make Hooks Idempotent When Possible#

If a brick can be run multiple times safely, it’s less error-prone:

void _addDependency(String dep) {
final lines = file.readAsLinesSync();
// Check if it already exists
if (lines.any((line) => line.contains(dep))) {
return; // Already added, skip
}
// Add it
_insertBeforeAnchor('#endregion', dep, lines);
}

5. Version Control Considerations#

In .gitignore:

# Mason cache
.mason/
# Lock file (if using local paths)
mason-lock.json

But do commit your bricks:

✅ bricks/
✅ mason.yaml

Your team needs the brick templates.

Creating Your Own Bricks: A Step-by-Step Guide#

Ready to create a brick for your project? Here’s the process:

Step 1: Identify the Pattern#

Look for repetitive tasks in your codebase:

  • Do you create similar widgets repeatedly?
  • Is there a common service pattern?
  • Do you have boilerplate for API endpoints?

Step 2: Create the Brick#

Terminal window
mason new my_brick

This generates the starter structure.

Step 3: Design Your Variables#

Edit brick.yaml:

vars:
name:
type: string
prompt: What is the name?
type:
type: enum
prompt: Select type
values:
- basic
- advanced
includeTests:
type: boolean
default: true
prompt: Include test files?

Step 4: Create Your Templates#

In __brick__/, create files with placeholders:

// __brick__/lib/{{name}}_service.dart
class {{name.pascalCase()}}Service {
{{#includeTests}}
// This appears only if includeTests is true
{{/includeTests}}
}

Step 5: Test Locally#

Terminal window
mason make my_brick

Iterate on the templates until they’re right.

Step 6: Add Hooks (Optional)#

If you need automation, create hooks/post_gen.dart:

import 'dart:io';
import 'package:mason/mason.dart';
void run(HookContext context) async {
final name = context.vars['name'];
print('Generated ${name}!');
// Add your automation here
}

Step 7: Add to mason.yaml#

bricks:
my_brick:
path: ./bricks/my_brick/

Step 8: Document It#

Add a README in your brick folder explaining:

  • What it generates
  • What variables it expects
  • What manual steps (if any) are needed

Step 9: Share with Your Team#

Commit the brick and teach your team to use it:

Terminal window
git add bricks/my_brick mason.yaml
git commit -m "feat: add my_brick for generating X"

Key Benefits & Takeaways#

After implementing Mason in production projects, here’s what teams typically experience:

1. Massive Time Savings#

Before Mason:

  • Creating a feature: 30-45 minutes
  • Creating a page: 15-20 minutes
  • Risk of mistakes: High

After Mason:

  • Creating a feature: 30 seconds
  • Creating a page: 15 seconds
  • Risk of mistakes: Near zero

Over a project with 20 features and 100 pages, that’s 50+ hours saved.

2. Consistent Architecture#

Every feature follows the exact same structure. New developers can navigate the codebase easily because everything is predictable.

3. Self-Documenting Codebase#

Your bricks become living documentation. Want to know the feature structure? Look at bricks/feature/__brick__/. Want to know integration points? Look at the hook.

4. Reduced Onboarding Time#

New team members don’t need to understand every integration point. They just run mason make feature and everything works.

5. Focus on Business Logic#

Instead of spending time on folder structure and wiring, developers immediately start working on the actual feature logic.

6. Scales with Complexity#

The more complex your architecture, the more Mason saves you. A simple app might save 20 hours per project. A complex enterprise app might save 200 hours.

7. Easy to Iterate#

Architecture changes? Update the brick, and all future features follow the new pattern. You can even provide migration bricks to update existing code.

Conclusion#

Mason transforms how you build Flutter applications. What starts as a simple templating tool becomes a powerful automation system that:

  • Eliminates repetitive boilerplate
  • Ensures architectural consistency
  • Reduces human error
  • Accelerates development
  • Makes your codebase more maintainable

The examples you’ve seen—the feature brick with 8 automated integration points, the page brick with conditional logic—are from a real production app. They save the team dozens of hours every month.

Start small: Create a brick for one repetitive pattern in your project. Maybe it’s a widget, a service, or a page template.

Grow your library: As you identify more patterns, add more bricks. Your brick collection becomes your team’s superpower.

Share with others: Open-source your bricks on BrickHub or blog about them. The Flutter community will thank you.

Mason isn’t just a tool—it’s an investment in your development workflow that pays dividends immediately and continues to pay off as your project grows.

Additional Resources#

Try It Yourself#

Ready to get started? Install Mason and create your first brick:

Terminal window
# Install Mason
dart pub global activate mason_cli
# Create a new brick
mason new my_first_brick
# Customize it, then run it
mason make my_first_brick

Happy coding, and may your boilerplate be forever automated! 🧱


This tutorial uses examples from a production Flutter application to demonstrate real-world Mason usage. The patterns shown are battle-tested across dozens of features and hundreds of pages.