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:
dart pub global activate mason_cliVerify the installation:
mason --versionYour 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:
mason new helloThis creates a brick structure:
bricks/hello/├── brick.yaml├── __brick__/│ └── hello.txt└── hooks/The brick.yaml defines your brick:
name: hellodescription: A simple hello brickversion: 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:
mason make hello# Prompt: What is your name? → JohnOutput (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:
- Creating a package structure with multiple folders
- Setting up localization files (potentially multiple languages)
- Configuring dependency injection
- Setting up routing
- Registering the feature in the main app’s:
pubspec.yaml- Localization delegates
- Router configuration
- Dependency injection container
- 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: featuredescription: Generate a complete feature moduleversion: 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
mason make feature? What is the feature name? (in snake_case) notificationsMason 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:
- Adds the feature to
pubspec.yaml - Registers localization delegates
- Registers routes
- Registers dependency injection
- 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 FeaturesThe 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 generationfinal 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.0Now 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: pagedescription: Generate a BLoC-based pageversion: 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:
- Enum Variable: The
featurevariable is an enum, so users select from existing features - Boolean Variable: Conditional logic based on whether this is an app-level route
- 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.dartTemplate 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 FeaturesYour 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 cleanclass {{name.pascalCase()}}Service {}
// Hook handles registrationvoid _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.jsonBut do commit your bricks:
✅ bricks/✅ mason.yamlYour 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
mason new my_brickThis 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.dartclass {{name.pascalCase()}}Service { {{#includeTests}} // This appears only if includeTests is true {{/includeTests}}}Step 5: Test Locally
mason make my_brickIterate 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:
git add bricks/my_brick mason.yamlgit 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
- Official Documentation: docs.brickhub.dev
- Mason GitHub: github.com/felangel/mason
- BrickHub: brickhub.dev - Public brick repository
- Flutter Package of the Week: Mason on YouTube
- Very Good Engineering Blog: Search for Mason articles
Try It Yourself
Ready to get started? Install Mason and create your first brick:
# Install Masondart pub global activate mason_cli
# Create a new brickmason new my_first_brick
# Customize it, then run itmason make my_first_brickHappy 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.