Flutter Code Generation: Getting Started

Learn how to use code generation to automatically create Dart models, eliminating tedious and repetitive tasks. By Aachman Garg.

5 (8) · 1 Review

Download materials
Save for later
Share

In software engineering, one of the key principles to maximize productivity is to never repeat yourself. For rapid and efficient development, you should automate repetitive tasks. Imagine having to write getters and setters for each model in a project with 100 models, manually serializing JSON or writing a clone for every class. That seems pretty exhausting. Enter code generation.

Code generation of Flutter models helps you automate these tedious tasks so you can focus on what’s important. All you have to do is write the repetitive code pattern once and create a generator, which generates files with code based on the pattern you defined.

In this tutorial, you’ll build a code generator that finds all the variables of a class, stores them in a map and generates getters and setters for saving to and reading from the map. You’ll also learn the following along the way:

  • When to use code generation.
  • How to define and use annotations.
  • How a generator writes code.
  • What to use ElementVisitor for.
  • How to configure a Builder with build.yaml.
  • How to generate the models in a Flutter project.
Note: This tutorial assumes you have basic knowledge of Flutter. If you’re new to Flutter, check out our book, Flutter Apprentice, or the Getting Started with Flutter tutorial. At the very least, you should know how to open a project in your favorite IDE, navigate the source code, initialize your packages with pub get and run your app in a simulator.

So, without any further delay, get started!

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial. Once downloaded, open the project in your IDE of choice and explore it.

Take a look at the folder structure — it’s essential for the project. You’ll take a closer look at this structure in the coming sections, but first, you should dive a bit into the theory.

Why Code Generation?

Code generation has several advantages under certain situations. Here are a few:

  • Data classes: These type of classes are fairly simple and you usually need to create a lot of them. So, it’s a good idea to generate these instead of manually writing each one.
  • Architecture boilerplate: Almost every architecture solution comes with some amount of boilerplate code. Writing it repeatedly is a headache that you can prevent, in large part, by generating the code. MobX is a good example of this.
  • Common features/functions: Almost every model class uses certain functions, like fromMap, toMap and copyWith. With code generation, the classes can all get these functions in a single command.

Code generation not only saves time and effort, but also improves code quality in terms of consistency and the number of errors. You can literally open any generated file with the assurance that it’ll work perfectly.

Setting up Your Initial Project

The project’s folder structure includes three top level folders: annotations, generators and example. You’ll take a look at each one next.

The Annotations Folder

Inside annotations is a lib folder, which will contain the Dart files for the annotations library. Its src folder will contain files that declare annotations the generators will use. Finally, pubspec.yaml defines the metadata for the annotations library. It’s very simple, with test: 1.3.4 as the only dependency it needs.

The Generators Folder

The lib folder contains the src subfolder for better organization of builder, model_visitor and generator files. More on these in a later section.

Take a look at pubspec.yaml:

name: generators
description: Getters and setters to save variables to and from Map
version: 1.0.0

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  # 1
  build: 
  # 2
  source_gen:
  # 3
  annotations:
    path: ../annotations/

dev_dependencies:
  # 4
  build_runner:
  # 5
  build_test:
  test: ^1.0.0

Here’s what’s going on above:

  1. build: A package that lets you inspect classes. It gives you access to different elements of a class like variables, methods and constructors. You’ll use this to extract the variable names for generating getters and setters in your project.
  2. source_gen: An API providing various utilities that help generate code without much interaction with the low level build or analyzer packages. This will make your life a lot easier.
  3. annotations: The library you’ll create soon. The generators use it to recognize which classes to process.
  4. build_runner: Allows the generation of files from Dart code. This is a dev_dependency; you’ll only use it during development.
  5. build_test and test: For testing the generators. You won’t use them in this tutorial.

Build.yaml

There’s one more file to be aware of: build.yaml. Here’s how it looks:

targets:
  $default:
    builders:
      generators|annotations:
        enabled: true

builders:
  generators:
    target: ":generators"
    # 1
    import: "package:generators/builder.dart"
    # 2
    builder_factories: ["generateSubclass", "generateExtension"]
    # 3
    build_extensions: { ".dart": [".g.dart"] }
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

This is the configuration that build_runner needs to find the generators. Here are some important things to note:

  1. import is the path to builder.dart, which you’ll create in a later section.
  2. builder_factories contains the method names of global functions that return generators. You’ll define them later.
  3. In build_extensions, you specify the extension of the generated file — “.g.dart”, in this case.
Note: If you want to learn more about build.yaml, refer to this article on the build.yaml format by the Dart team.

The Example Folder

example is the Flutter project in which you’ll use the generator. For now, just leave it and focus on the other two folders.

Creating the Necessary Annotations

Annotations are data classes that specify additional information about a code component. They provide a way to add metadata to code elements like a class, method or variable.

Take, for example, the @override annotation, which you use when implementing a method from a parent class in a child class. By using it, you’re explicitly specifying that this method is from the parent class.

Note: Annotations always begin with the @ sign.

Creating the Annotation Classes

Go to annotations/lib/src and create a new file named subclass_method.dart. Next, add the following code:

class SubclassAnnotation {
  const SubclassAnnotation();
}

const generateSubclass = SubclassAnnotation();

The class is blank because, in this project’s use case, you don’t need any additional data in the annotation. The global variable generateSubclass is the name of the annotation. You’ll use this name to mark a class for a generator. You can create annotations from any class that has a const constructor.

Similarly, create another file named extension_method.dart in the same folder and add the following code:

class ExtensionAnnotation {
  const ExtensionAnnotation();
}

const generateExtension = ExtensionAnnotation();

At this point, you’ve written both annotations, but you’re missing one final step. Create annotations.dart in lib with the following code:

library annotations;

export 'src/subclass_method.dart';
export 'src/extension_method.dart';

In the code above, you export the two files of the package.

Awesome! The annotations library is complete. Next, you’ll move on to creating the generators.