Mason — A Complete and Comprehensive Guide
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.
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!