How Can You Deliver Flutter Apps Faster with App Templates and Mason?
Are you tired of starting your Flutter projects from scratch? Do you wish there was an easy way to speed up the initial setup and reuse the code across the projects?
The mason_cli tool may be just what you need to streamline your Flutter development process. Find out how Flutter mason_cli can help you create and use Flutter app templates.
What are app templates?
A template is a pre-designed and pre-coded project that you can use as a starting point for building your own mobile application. App templates often include the user interface, basic functionalities, and other features that are commonly found in many mobile apps. By using an app template, you save time and effort. So, you can focus on customizing the product to meet your specific needs and requirements.
What is mason?
The mason package is a Dart template generator that helps teams quickly create files. This custom code generation tool allows developers to build their own app templates called “bricks” using the mustache template notation which is language-agnostic.
This means that developers can use it to create such templates for any type of programming language. It’s especially valuable when the team uses one framework for a variety of projects.
What’s more, mason is an open-source solution developed and maintained by a community of contributors. It’s continuously evolving and improving. This makes mason a suitable tool for anyone who wants to optimize the development process.
Pros of using mason
- You can quickly make the initial setup for your projects and create a foundation for the app’s infrastructure with just one simple command.
- Your time is important, so you need to reduce the quantity of boilerplate code written manually. That’s what serialization packages and other code-generation tools are for. Keep in mind that they must be used in a specific manner to create predefined methods, classes, etc.
- Bricks are easily accessible. BrickHub is a popular example of a platform that enables it but there are many different templates you can choose from.
- With mason, you can generate your own code that addresses your unique needs.
Imagine you need to develop a complex Flutter app. In big projects, a good foundation is key. Even if you copy things from one file to another this process is going to take a lot of time. And think of how repetitive and boring this task would be. It’s better to quickly use reusable templates instead. It allows you to save your precious time and spend it on cool things like animations.
Cons of using mason when building mobile apps
The biggest challenge is maintaining and editing the brick, so it would better suit one’s needs. How is that a problem? Writing code with mustache notation is hard due to the errors you’d probably face. And there won’t be much help coming from your IDE. The only solution is to install the extension that handles mustache.
It can lead to some troubles in case of complex implementations containing multiple files. For example, if you use statements to generate files, your project structure would be hard to read. There’s a chance it would lead to mistakes when you’d like to add source code to correct the directories.
Creating the brick templates: the basic step in using Flutter templates
Mason can be installed via pub.dev or Homebrew. Once it’s set up, mason is available as a command that you can run from the terminal.
# ? Activate from https://pub.dev
dart pub global activate mason_cli
# ? Or install from https://brew.sh
brew tap felangel/mason
brew install mason
Brick templates consist of a “brick.yaml” file and a “brick” directory that contains the template code. When you generate the code from a brick, mason gathers the input variables needed for this brick (either via prompts or as command line arguments). Then, it embeds them into the brick before writing the newly generated code to disk. Since mason is a command-line interface (CLI) tool, you can run it straight from your terminal.
Let’s start with the “hello world” example. It’s included out of the box when you initialize mason. To try it out, run “mason make hello” in your terminal. After asking for input to replace the brick variable, mason will generate a “HELLO.md” file in the current directory with the text “Hello, world!”. This is a simple example, but it shows how you can use mason to quickly generate code from a template.
Mason and mobile app templates usage – examples
To create a new mason directory, developers can run the command “mason new [brick_name] –hooks”. It generates a series of files and the “hooks” folder. This is where you define whether the scripts should run before or after the creation of the brick.
The brick folder contains everything related to the template along with the README, LICENSE, and CHANGELOG files. You can specify a custom output directory for your template and all created files. They make it possible to prepare an accurate description and to share your bricks among many people (e.g. via Brickhub). Existing brick templates can speed up your development significantly. New bricks are published there daily.
One example of using mason is creating a template to build a model in a Flutter app.
Writing a model can be tedious as it requires a fair amount of boilerplate code (particularly when using json_serializable) and splitting it for domain and data layer. With mason, developers can create a template that generates the necessary code, including “import statements” and the @JsonSerializable() annotation.
How do you use mason in practice? First, you need to configure your brick template. Let’s jump to our config file brick.yaml file to do so.
name: mason_example_model
description: mason article model template
# The following defines the version and build number for your brick.
# A version number is three numbers separated by dots, like 1.2.34
# followed by an optional build number (separated by a +).
version: 0.2.0
# The following defines the environment for the current brick.
# It includes the version of mason that the brick requires.
environment:
mason: ">=0.1.0-dev.41 <0.1.0"
# Variables specify dynamic values that your brick depends on.
# Zero or more variables can be specified for a given brick.
# Each variable has:
# * a type (string, number, boolean, enum, or array)
# * an optional short description
# * an optional default value
# * an optional list of default values (array only)
# * an optional prompt phrase used when asking for the variable
# * a list of values (enums only)
vars:
modelName:
type: string
description: Model name
default: model
prompt: Insert the model name
Now, you can start working on the structure of your files:
? brick
┣ ? lib
┃ ┣ ? data
┃ ┃ ┗ ? {{modelName.snakeCase()}}_dto.dart
┃ ┣ ? domain
┃ ┃ ┗ ? {{modelName.snakeCase()}}.dart
After structuring your brick directory properly and running brick from the root of the project, create data and domain folders. Then, add all other models to these folders.
But first, let’s discuss .snakeCase(). If you are familiar with the Dart naming convention, you know that the snake case naming style should be applied to files. To achieve this (or to apply any other naming convention), you can use built-in lambdas that ship with mason. All available notations are in the documentation. With that out of the way, go to {{modelName.snakeCase()}}_dto.dart.
import 'package:json_annotation/json_annotation.dart';
part '{{modelName.snakeCase()}}_dto.g.dart';
@JsonSerializable()
class {{modelName.pascalCase()}}Dto {
String id;
{{modelName.pascalCase()}}Dto(
this.id,
);
factory {{modelName.pascalCase()}}Dto.fromJson(Map<String, dynamic> json) => _${{modelName.pascalCase()}}DtoFromJson(json);
Map<String, dynamic> toJson() => _${{modelName.pascalCase()}}DtoToJson(this);
}
Like the files, the classes use pascalCase(). So, you need to change them accordingly.
As you can see, your template currently contains just one String. But you can add an array of properties as a variable for your own brick! This way you are also creating methods for out-of-the-box serialization.
After completing this task, go to domain model implementation.
import 'package:equatable/equatable.dart';
class {{modelName.pascalCase()}} extends Equatable {
final String id;
const {{modelName.pascalCase()}}({
required this.id,
});
@override
List<Object> get props => [
id,
];
}
To use the brick, developers can run the command “mason generate [brick_name] [output_path]”. You just need to enter the name of the brick and the desired output path (or create it in the current working directory). This will generate the necessary files with the value input from given prompts according to the Flutter app template.
Mason speeds up the process of generating repetitive code. It also helps teams maintain consistency in their codebase. By defining a template and using it across all the projects, you make sure that the whole code follows the same structure and conventions.
But currently, the code contains errors. This is due to the fact that @JsonSerializable() needs to be generated by the “flutter pub run build_runner build –delete-conflicting-outputs” command. And here comes the hooks’ functionality! They are nothing other than scripts that run before the model is generated (pre_gen.dart) and afterward (post_gen.dart). This case requires you to run a generation code for serialization when the model is created. So, let’s open the post_gen.dart file.
As you can see, you are welcomed with a run method that contains HookContext. It gives you access to brick variables or functionalities like mason_logger. To run the script in the terminal, simply call Process from the dart:io package.
import 'dart:io';
import 'package:mason/mason.dart';
void run(HookContext context) async {
final codeGen = context.logger.progress('Build runner gen in progress');
await Process.run(
'flutter',
['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'],
runInShell: true,
);
codeGen.complete();
When you regenerate your brick, you should see the error-free code when the script is executed. If you are wondering if the result is worth the effort, I’ll lay out a more complex example with models that introduce statements to your bricks.
How to handle complex projects with app brick templates?
Every development process – either frontend or backend – leads to statements. Mobile apps can contain hundreds of screens. To keep code maintainable and to provide a clean UI, you divide files in a certain way. Code structure in a simple app can fit in just a few folders. But in complex products, directories and files can pile up. If you are not maintaining a pre-established order, some elements might get lost.
Let’s use the example with a domain and data layer. Add some folders to scale up complexity and cases to build models for requests and responses separately. In the domain layer, decide whether you want to create a separate folder (e.g., a screen containing a few models).
Start with laying out a file structure, for example, for the users class. You need to use conditions in order for the template to determine if there is a need to generate files or not. Mason handles statements by boolean variables, so the next step is to add these statements to brick.yaml.
createDirectory:
type: boolean
description: Indicator if model has it's own directory
default: false
prompt: Do you want to create directory for model?
createToJson:
type: boolean
description: Indicator if model has toJson() method
default: false
prompt: Do you want to create toJson() for model?
createFromJson:
type: boolean
description: Indicator if model has fromJson() method
default: false
prompt: Do you want to create fromJson() for model?
Then, jump to the project and start working with the condition syntax {{#statement}}fileName.dart{{/statement}}. The only downside of this solution is that the mustache notation is used as a closing bracket for a statement. Using ‘/’ tells your file system to go into folders. It will be displayed in your IDE like this:
? brick
┗ ? {{#statement}}fileName.dart{{
┃ ┗ ? statement}}
If a statement is true, the generator will create the right file as:
? Output
┗ ? fileName.dart
That is why I suggest adding README to each brick that you create with the file tree. This solution will also help other developers who use your template. Then, you’ll obtain a result similar to this:
? brick
┗ ? {{#statement}}fileName.type{{/statement}}
Complex model solution
? __brick__
┣ ? lib
┃ ┣ ? communication
┃ ┃ ┣ ? network
┃ ┃ ┃ ┣ ? dto
┃ ┃ ┃ ┃ ┣ ? {{modelName.snakeCase()}}
┃ ┃ ┃ ┃ ┃ ┣ ? {{#createToJson}}requests{{/createToJson}}
┃ ┃ ┃ ┃ ┃ ┃ ┗ ? {{modelName.snakeCase()}}_request_dto.dart
┃ ┃ ┃ ┃ ┃ ┣ ? {{#createFromJson}}responses{{/createFromJson}}
┃ ┃ ┃ ┃ ┃ ┃ ┗ ? {{modelName.snakeCase()}}_response_dto.dart
┃ ┣ ? domain
┃ ┃ ┣ ? models
┃ ┃ ┃ ┣ ? {{#createDirectory}}{{modelName.snakeCase()}}{{/createDirectory}}
┃ ┃ ┃ ┃ ┗ ? {{modelName.snakeCase()}}.dart
┃ ┃ ┃ ┗ ? {{^createDirectory}}{{modelName.snakeCase()}}.dart{{/createDirectory}}
You need to know that # and ^ characters indicate whether the statement is true or false. Request and response directories with files will be created only if the corresponding booleans are true. On the other hand, domain models will be created in all cases, either in a separate directory or in models.
What else can you use mason for?
In addition to models, mason can also be used to generate other types of code and components in a Flutter app. For example, developers can create a brick for a custom widget which can include the necessary import statements and code for the widget’s constructors, properties, and methods. This way they save time and reduce the amount of repetitive code that needs to be written for each widget.
Another way to use mason is to create a brick for a page or screen in the app. It can include the necessary import statements, as well as the code for the page’s layout, widgets, and functionalities. It’s especially recommended in the case of pages that follow a similar structure or have similar elements, such as a login page or a settings page.
You can also try mason to generate code for common app features, such as networking, localization, and state management.
It’s a way to save a lot of time because there’s no need to write the same code multiple times. This can be useful especially for Firebase integrations or some complex layouts that contain video or podcast players.
Flutter templates: conclusions
Overall, mason provides a wide range of possibilities for generating code in a Flutter app. By creating custom templates and using them across the projects, teams save time and ensure consistency in their codebase. They also improve the development process and the quality of the final product.
The mason_cli tool also allows specialists to create and use their own app templates called “bricks” with the mustache template notation.
This solution is definitely worth your attention if you want to take the most out of the time dedicated to the project. If you’d like to know more about mason and templates, feel free to write me a comment.