Mason — A Complete and Comprehensive Guide

Hadiyaaamir
12 min readNov 14, 2023

--

Ever found yourself following nearly the exact same steps every time you start setting up a project? Or repeating the setup procedure when adding a new feature to your application? Wishing you could somehow automate this tedious process, and instead focus on the more creative and essential aspects of app development?

If you’ve ever encountered these scenarios, then Mason might just be the thing for you!

What is Mason?

Brainchild of Felix Angelov, Mason is a Dart package that simplifies code generation by using predefined templates called “bricks”. With Mason, developers can automate repetitive tasks, maintain code consistency, and reduce boilerplate code, thus streamlining the workflow, and making it an essential tool for efficient Flutter app development.

Using Mason Bricks

Before you dive headfirst into creating a custom code template, take a moment to explore the vast treasure trove of ready-made bricks available through Mason.

The gateway to this wealth of pre-built resources is Brickhub, an online platform that serves as a central hub for the Flutter developer community. Here, you’ll find an extensive collection of bricks meticulously crafted to address a wide range of development needs. From bricks designed to create a starter app with ease to those that streamline the process of generating a model, there is a vast variety of templates available at your fingertips.

1. Setup

First, make sure you have the mason_cli installed

# 🎯 Activate from https://pub.dev
dart pub global activate mason_cli
# 🍺 Or install from https://brew.sh
brew tap felangel/mason
brew install mason

2. Initialisation

Then, in your project, initialise mason using the following command. This creates a mason.yaml file, which is where bricks will be registered.

# Initialise mason
mason init

3. Installation

Once you’ve set up Mason, you can decide on a brick to use and add it. Bricks can be added from various sources: BrickHub, a GitHub repository, or a local path if you’ve created a custom brick.

Using the Command Line

One way to add a brick is directly from the command line.

# From BrickHub
# Example: greeting (https://brickhub.dev/bricks/greeting)
mason add greeting
# From GitHub
# Provide path to then directory in which brick.yaml is located
mason add brick_name --git-url='https://github.com/username/repository-name.git' --git-path='bricks/brick_name' --git-ref='branch'
# From a Local path
mason add brick_name --path='path/to/your/brick'

Add Brick Directly to mason.yaml

Alternatively, you can register the brick by adding it directly to your mason.yaml file

bricks:
greeting: "0.1.0+2"
github_brick:
git:
url: 'https://github.com/username/repository-name.git'
path: 'bricks/github_brick'
custom_brick:
path: path/to/your/brick/custom_brick

Then, run this command to install all the bricks in your mason.yaml

mason get

Adding Bricks Globally

When you leave your workspace, you may find it inconvenient that your installed bricks are no longer available. However, there’s a solution to this. You can install bricks globally, making them accessible from any location on your machine. All it takes is a simple addition of the “-g” flag to your commands.

# Install the greeting brick from BrickHub globally
mason add -g greeting

4. Usage

Run the mason make command to use your installed template in your current directory

# Use the greeting brick
mason make greeting

Creating Your Own Brick

While ready-made bricks offer immense convenience, there may come a time when your project demands a custom solution that’s uniquely tailored to your specific needs.

In this section, we’ll explore how you can craft your own brick, allowing you to define your code templates and streamline the development process with precision.

Setup

Use the following command to create a new brick with a specified name (replace brick_name with your chosen name):

# Create a new brick
mason new brick_name

This command will generate a new folder for your custom brick, which includes a __brick__ folder and a brick.yaml file. These elements serve as the foundation for your custom template.

Creating the Template

Once you’ve set up your custom brick using the mason new command, it's time to shape the template according to your project's needs.

The __brick__ Folder

The __brick__ folder serves as the heart of your custom brick template. Anything you add to this folder will be mirrored in the target directory when you generate code using your brick. Adding the necessary code, files, and structure to the __brick__ folder is what enables you to create your template, as this will be used as the blueprint for generating code in the target directory.

Example: Adding a File

Let’s illustrate how to add a simple file to your custom brick template. Suppose you want your custom brick to generate a file named example_file.dart in the lib folder of your target directory. Here's what you'd do:

Inside the __brick__ folder, create a new directory named lib. Within the lib directory, create a new file named example_file.dart and add the necessary code or content. This is where your generated file will reside:

# Directory structure inside the custom brick

brick_name
|
└── __brick__
|
├── lib
│ └── example_file.dart
...

Now, when you use your custom brick to generate code, it will create a directory structure in the target directory, and the example_file.dart will be placed in the lib directory of the target. If it doesn’t already exist, a lib folder will be created. The code you added to example_file.dart in your custom brick's __brick__ folder will also be mirrored in the generated file.

# Generate the brick
mason make brick_name
# Output Structure in Target Directory

target_directory
|
...

├── lib
│ └── example_file.dart
...

By defining the structure and content in the __brick__ folder, you can create templates that generate complete files, directories, or even entire components, empowering you to streamline your development process effectively.

Adding Variables

What if you wanted to create a customised, adaptable template that evolves with your unique needs? Variables in Mason are the key to making your custom brick templates dynamic and tailored to your project. They provide flexibility, making your development process more efficient.

Variable Types

Mason offers a range of variable types to precisely customize your templates:

  • String: For text data.
  • Number: Handling numerical values.
  • Boolean: Managing true or false values.
  • Enum: Enabling selection from predefined values (exactly one value).
  • Array: Dealing with collections of values, which can be predefined, and users can select as many as needed.

Registering Variables in brick.yaml

To utilise variables in your custom brick, the first step is to register them in your brick.yaml file. Each variable configuration should include:

  • Variable Name: A unique identifier used to reference the variable in your templates.
  • Type: Specifies the variable type
  • Description (Optional): A brief description to clarify the variable’s purpose.
  • Default Value(s) (Optional): Default values used when the user doesn’t provide an explicit value. For arrays and enums, you can specify multiple defaults.
  • Prompt (Optional): The question or message presented to the user when they input values for the variable.
  • Values (Enums/Arrays Only, Optional): For enum and array variables, list the available options for the user to choose from.
# brick.yaml file

name: sample_template
description: A sample custom template created with Mason.
version: 0.1.0+1
environment:
mason: ">=0.1.0-dev.51 <0.1.0"

# Define variables here
vars:
appTitle:
type: string
description: The title of your application
default: My App
prompt: What is the title of your application?

numPages:
type: number
description: Number of pages in your application
default: 5
prompt: How many pages do you want in your application?

includeLogo:
type: boolean
description: Include a logo in your application?
default: true
prompt: Do you want to include a logo in your application?

osType:
type: enum
description: Choose the operating system type
values:
- Android
- iOS
prompt: Select the operating system for your application

features:
type: array
description: Select the features you want to include
defaults:
- authentication
values:
- authentication
- push notifications
- in-app purchases
prompt: Which features would you like to include in your application?

User Interaction

When you run the mason make command to generate code using your custom brick, the user will be prompted to provide values for the registered variables. The questions displayed are the prompts you've defined in your brick.yaml.

For enum and array variables, a dropdown-like interface is presented, allowing users to navigate and select options using arrow keys and the space bar.

If a user presses enter without providing a value, the default value you’ve set for the variable will be used.

Mustache Syntax — Using the Variables

Now that you’ve registered and configured variables in your brick.yaml file, it's time to put them to use within your templates.

Mason leverages the Mustache syntax for rendering variables in your generated code. Mustache acts as the bridge between your registered variables and the dynamic content you want to include in your templates.

Using Variables Directly

To insert a variable directly into your template, simply enclose its name within double curly braces using Mustache syntax. For instance, if you have a variable named appTitle in your brick.yaml, you can include it in your template like this:

# Using the appTitle in pubspec.yaml

name: {{appTitle}}
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1

Conditionals

Booleans are often used to control conditional logic in your templates. Let’s say you have a customTheme boolean variable. You can use it to conditionally include or exclude specific code blocks:

// Add code if condition is true

{{#customTheme}}
// Code for custom theme
{{/customTheme}}
// Add code if condition is false

{{^customTheme}}
//Code for non-custom theme
{{/customTheme}}

Suppose you want to add files and folders also based on some condition. The same syntax {{#booleanVariable}} for checking true, and {{^booleanVariable}} for checking false may be used.

To do this, alter the file/folder name and enclose them with this mustache conditional.

The theme folder will be generated only if the customTheme variable is true

Looping

Looping is useful when you want to repeat content for each element in an array. Suppose you have an array of platforms, and you want to list all the platforms supported in your README:

## Platforms
{{#platforms}}
- {{.}}
{{/platforms}}

In this example, {{#platforms}} starts the loop, and {{/platforms}} ends it. The {{.}} represents the current value in the array, so all values in your array will be listed as bullet points.

Escaping Special Characters

In some cases, you might have to escape special characters. Some such cases include using a variable within a dart String, or when using a path variable. This can be done simply by using triple curly braces {{{variableName}}} instead of double curly braces {{variableName}} when dealing with special characters.

String title = '{{{appTitle}}}';
String path = '{{{imagePath}}}'

Lambdas

Lambdas in Mustache serve as powerful tools to apply functions to variable values. They empower you to modify or format variable content before it’s rendered. In the context of Mason, various built-in lambdas are available to facilitate this process. These lambdas include: camelCase, constantCase, dotCase, headerCase, lowerCase, pascalCase, paramCase, pathCase, sentenceCase, snakeCase, titleCase, and upperCase.

Imagine a scenario where you need to generate class names based on a variable called featureName. Here is where you might find the pascalCase lambda useful, as classes usually follow this format.

You can use these lambdas in one of two ways:

1. Enclose the variable between {{#lambdaName}} and {{/lambdaName}} tags to apply the function.

class {{#pascalCase}}{{featureName}}{{/pascalCase}}View extends StatelessWidget {
const {{#pascalCase}}{{featureName}}{{/pascalCase}}View({super.key});

@override
Widget build(BuildContext context) {
return const {{#pascalCase}}{{featureName}}{{/pascalCase}}Body();
}
}

2. Call the lambda as a function on a variable.

class {{featureName.pascalCase()}}View extends StatelessWidget {
const {{featureName.pascalCase()}}View({super.key});

@override
Widget build(BuildContext context) {
return const {{featureName.pascalCase()}}Body();
}
}

You may also name your files and folders this way. For example, you’ll probably want to name the file containing the above code as {{featureName.snakeCase()}}_view.dart.

Pre-gen and Post-gen Hooks

Mason empowers developers with the flexibility to customise the code generation process through pre-gen and post-gen hooks. These hooks allow you to execute scripts or commands before and after generating code, adding a layer of dynamism to your templates.

Setting Up a Brick With Hooks

To incorporate hooks into your brick, use the following command:

mason new brick --hooks

This generates a hooks folder containing pre_gen.dart and post_gen.dart files, which serve as the entry points for pre-gen and post-gen hooks.

Pre-gen Hooks

Pre-gen hooks execute before the code generation process begins, enabling you to perform tasks such as setting up environment variables, initializing project configurations, or running scripts necessary for code generation.

Consider a scenario where you collect user input as an array. To dynamically adjust your template based on the array’s content, pre-gen hooks come in handy. Here’s an example:

import 'package:mason/mason.dart';

void run(HookContext context) {
final List validators = context.vars['validators'];
context.vars['hasValidators'] = validators.isNotEmpty;

context.vars['email'] = validators.contains('email');
context.vars['username'] = validators.contains('username');
context.vars['password'] = validators.contains('password');
context.vars['confirmedPassword'] = validators.contains('confirmed password');
context.vars['stringInput'] = validators.contains('string input');
context.vars['numericInput'] = validators.contains('numeric input');
}

In this example, boolean variables are set up based on the user’s array input, allowing you to use them to conditionally include or exclude specific code blocks in your templates.

Post-gen Hooks

Post-gen hooks run after Mason generates the code, providing an opportunity to perform tasks such as applying code formatting, testing generated code, or executing operations needed to complete the code generation process.

Here’s an example demonstrating how you might use a post-gen hook to add dependencies and fetch them:

Future<void> run(HookContext context) async {
final appName = context.vars['appName'].toString().snakeCase;
final appDirectory = Directory('${Directory.current.path}/$appName');

final cdCommand = 'cd $appDirectory';

// Add dependencies
await Future.wait([
Process.run('bash', ['-c', '$cdCommand && flutter pub add bloc']);
Process.run('bash', ['-c', '$cdCommand && flutter pub add flutter_bloc']);
Process.run('bash', ['-c', '$cdCommand && flutter pub add equatable']);
)];

// Get dependencies
await Process.run('bash', ['-c', '$cdCommand && flutter pub get']);
}

This example demonstrates how post-gen hooks can be utilized to automate tasks such as adding and fetching dependencies after the code is generated.

Generating Bricks Programmatically

While Mason provides a powerful command-line interface for generating bricks, it also offers the flexibility to automate the process using Dart code. This approach enables developers to programmatically create bricks, tailor them to specific project needs, and seamlessly integrate them into their workflow. Let’s explore how you can harness the programmability of Dart to generate bricks effortlessly.

1. Add and import the mason package as a dependency

To get started, add the mason package as a dependency in your project. Import the package in your relevant file to use it.

import 'dart:io';
import 'package:mason/mason.dart'; // import mason

2. Obtain the Brick

Specify the source of your brick, whether it’s a local path, a GitHub repository, or fetched from BrickHub.

// From a Local Path
final brick = Brick.path('/path/to/your/brick');
// From Github
final brick = Brick.git(
GitPath(
'https://github.com/yourusername/yourbrickrepository.git',
path: 'path/to/your/brick', // (optional)
ref: 'branch-name', // (optional)
),
);
// From Brickhub:
final brick = Brick.version(name: 'name', version: 'version');

3. Variables

Create a Map<String, dynamic> to store the variables you need to pass to your brick. The variables here have been hardcoded. However, you may also obtain them in any other way such as through user input.

Map<String, dynamic> variables = <String, dynamic>{
'appTitle': 'My App',
'numPages': 3,
'includeLogo': false,
'osType': 'iOS',
'features': ['authentication', 'push notifications'],
};

4. Generate the Brick

Initialise the generator and target, run the pre-gen hook (if applicable), generate the brick based on your specifications, and execute the post-gen hook (if applicable).

// Initialise the generator and target
final generator = await MasonGenerator.fromBrick(brick);
final target = DirectoryGeneratorTarget(Directory.current);
// Run the pre-gen hook (if applicable)
await generator.hooks.preGen(
vars: variables,
onVarsChanged: (vars) => variables = vars,
);
// Generate the brick based on your specifications
await generator.generate(target, vars: variables);
// Execute the post-gen hook (if applicable).
await generator.hooks.postGen(vars: variables);

Example Template

Here’s a complete template you may use as a reference:

import 'dart:io';
import 'package:mason/mason.dart';

Future<void> main() async {
final brick = Brick.git(
GitPath(
'https://github.com/username/repository-name.git',
path: 'bricks/github_brick',
),
);

Map<String, dynamic> variables = <String, dynamic>{
'appTitle': 'My App',
'numPages': 3,
'includeLogo': false,
'osType': 'iOS',
'features': ['authentication', 'push notifications'],
};

final generator = await MasonGenerator.fromBrick(brick);
final target = DirectoryGeneratorTarget(Directory.current);

// Pre-gen hook
final pregenProgress = Logger().progress('Compiling pre-gen hook...');
await generator.hooks.preGen(
vars: variables,
onVarsChanged: (vars) => variables = vars,
);
pregenProgress.complete('Compiled pre-gen hook');

// Brick
final brickProgress = Logger().progress('Generating brick...');
await generator.generate(target, vars: variables);
brickProgress.complete('Brick generated');

// Post-gen hook
final postgenProgress = Logger().progress('Compiling post-gen hook...');
await generator.hooks.postGen(vars: variables);
postgenProgress.complete('Compiled post-gen hook');
}

Conclusion

In summary, Mason emerges as a powerful companion for Flutter developers, seamlessly integrating automation, customisation, and efficiency into the development process.

This guide serves as a stepping stone, unlocking the immense potential within Mason and empowering you to navigate the rich landscape of code generation. As you embark on your coding endeavors, may Mason’s capabilities spark creativity and streamline your Flutter development journey.

Happy coding!

--

--