UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Flutter Tutorials

Flutter Code Generation: Getting Started

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

5/5 3 Ratings

Version

  • Dart 2.7, Flutter 1.22, Android Studio 4.2

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.

Creating the Generators

This is where the magic happens. The generators library will contain all the implementation details for generating code. This consists of four files. Next, you’ll go through them one-by-one.

Finding Annotated Classes With ModelVisitor

In lib/src, create model_visitor.dart with the following imports:

import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/dart/element/element.dart';

Here, you import visitor and element from analyzer. visitor provides SimpleElementVisitor, which lets you inspect classes. element provides an API to access different class elements, like FieldElement and MethodElement.

Note:
If you haven’t gotten the dependencies yet, now’s the time to do so. Run flutter pub get in the generators folder.

Below the imports, add this code:

// 1
class ModelVisitor extends SimpleElementVisitor<void> {
  // 2
  String className;
  final fields = <String, dynamic>{};

  // 3
  @override
  void visitConstructorElement(ConstructorElement element) {
    final elementReturnType = element.type.returnType.toString();
    // 4
    className = elementReturnType.replaceFirst('*', '');
  }

  // 5
  @override
  void visitFieldElement(FieldElement element) {
    final elementType = element.type.toString();
    // 7
    fields[element.name] = elementType.replaceFirst('*', '');
  }
}

Here’s what you do in the code above:

  1. You create the class, ModelVisitor, that extends SimpleElementVisitor. SimpleElementVisitor has most of the methods you need already implemented.
  2. For this project, you need to access the class name and all the variable fields, so you add these variables to the class. fields is a map with the variable’s name as key and its datatype as value. You’ll need both to generate getters and setters.
  3. You override visitConstructorElement to obtain the className by accessing type.returnType of each found constructor.
  4. elementReturnType ends with *, which you need to remove for the generated code to be accurate.
  5. visitFieldElement fills fields with the names and datatypes of all the variables found in the target class.
  6. Again, elementType ends with *, which you remove.

Cool! Now that you have the ingredients, you can start cooking. :]

Implementing a Generator for a Subclass

The first generator you build generates a subclass that implements all the getters and setters. Create subclass_generator.dart in lib/src and, as always, start with the import statements:

import 'package:build/src/builder/build_step.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/source_gen.dart';

import 'package:annotations/annotations.dart';

import 'model_visitor.dart';

Next, create SubclassGenerator extending GeneratorForAnnotation.

class SubclassGenerator extends GeneratorForAnnotation<SubclassAnnotation> {}

GeneratorForAnnotation gets the generic type parameter SubclassAnnotation, which is from the annotations library you created earlier. Basically, this is where you map the generator to the corresponding annotation.

To generate the source code from a class, implement the following method in the class:

// 1
@override
String generateForAnnotatedElement(
    Element element, ConstantReader annotation, BuildStep buildStep) {

  // 2
  final visitor = ModelVisitor();
  element.visitChildren(visitor); // Visits all the children of element in no particular order.

  // 3
  final className = '${visitor.className}Gen'; // EX: 'ModelGen' for 'Model'.

  // 4
  final classBuffer = StringBuffer();

  // 5
  classBuffer.writeln('class $className extends ${visitor.className} {');

  // 6
  classBuffer.writeln('Map<String, dynamic> variables = {};');

  // 7
  classBuffer.writeln('$className() {');

  // 8
  for (final field in visitor.fields.keys) {
    // remove '_' from private variables
    final variable =
        field.startsWith('_') ? field.replaceFirst('_', '') : field;

    classBuffer.writeln("variables['${variable}'] = super.$field;");
    // EX: variables['name'] = super._name;
  }

  // 9
  classBuffer.writeln('}');

  // 10
  generateGettersAndSetters(visitor, classBuffer);

  // 11
  classBuffer.writeln('}');

  // 12
  return classBuffer.toString();
}

Here’s what’s going on in the code above:

  1. You override generateForAnnotatedElement, which takes an element. In this case, that element is a class. You don’t need the other parameters in this simple case. The returned String contains the generated code.
  2. Start by visiting the class’s children.
  3. Then, create classname for the generated class.
  4. Because you need to work with a lot of Strings, using a StringBuffer is a good option.
  5. This is the point where you start writing the generated code lines. Create the class that extends the target class.
  6. Next, create the variables map that will store all of target class’s variables.
  7. Add the constructor of the class.
  8. Assign the target class’s variables to the map. field represents the variable’s name.
  9. End the constructor body.
  10. Call generateGettersAndSetters — well, to generate the getters and setters of all the variables.
  11. Close the constructor.
  12. Return the generated code as a single string.

Right below, add the following definition:

void generateGettersAndSetters(
      ModelVisitor visitor, StringBuffer classBuffer) {

// 1
for (final field in visitor.fields.keys) {
  
  // 2
  final variable =
      field.startsWith('_') ? field.replaceFirst('_', '') : field;

  // 3
  classBuffer.writeln(
      "${visitor.fields[field]} get $variable => variables['$variable'];");
  // EX: String get name => variables['name'];

  // 4
  classBuffer
      .writeln('set $variable(${visitor.fields[field]} $variable) {');
  classBuffer.writeln('super.$field = $variable;');
  classBuffer.writeln("variables['$variable'] = $variable;");
  classBuffer.writeln('}');

  // EX: set name(String name) {
  //       super._name = name;
  //       variables['name'] = name;
  //     }
  }
}

Here’s what you do in the code above:

  1. You loop over all variable names.
  2. Here, you remove _ from the private variables of the base class.
  3. This writes the getter code. visitor.fields[field] represents the variable’s datatype.
  4. This writes the code for the setter.

Done! Your first generator is ready, so move on to the second one.

Implementing a Generator for an Extension

This time, you’ll generate the getters and setters for each variable as methods of an extension. Although this is a different approach from what you used above, it achieves the same goal, so most of the code will be the same as in SubclassGenerator.

Create extension_generator.dart in lib/src and enter the following code:

// 1
import 'package:build/src/builder/build_step.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/source_gen.dart';

import 'package:annotations/annotations.dart';

import 'model_visitor.dart';

// 2
class ExtensionGenerator extends GeneratorForAnnotation<ExtensionAnnotation> {

  // 3
  @override
  String generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) {

  // 4
  final visitor = ModelVisitor();
  element.visitChildren(visitor);

  final classBuffer = StringBuffer();

  // 5
  classBuffer.writeln('extension GeneratedModel on ${visitor.className} {');
  // EX: extension GeneratedModel on Model {

  // 6
  classBuffer.writeln('Map<String, dynamic> get variables => {');

  // 7
  for (final field in visitor.fields.keys) {
    final variable =
        field.startsWith('_') ? field.replaceFirst('_', '') : field;

    classBuffer.writeln("'$variable': $field,"); // EX: 'name': _name,
  }

  // 8
  classBuffer.writeln('};');
  
  // 9
  generateGettersAndSetters(visitor, classBuffer);

  // 10
  classBuffer.writeln('}');

  // 11
  return classBuffer.toString();
  }
}

In the code above, you:

  1. Import the necessary packages.
  2. Create the class with ExtensionAnnotation as a generic type parameter.
  3. Implement the same method as you did in SubclassGenerator.
  4. Visit the class children and initialize StringBuffer.
  5. Here comes the difference! Start the extension GeneratedModel.
  6. Start the getter for the variables map.
  7. Add entries to the variables map.
  8. End the getter for the map.
  9. Again call generateGettersAndSetters.
  10. This ends the definition of the extension.
  11. Return the generated code.

As a second method of the class, add the following code:

void generateGettersAndSetters(
      ModelVisitor visitor, StringBuffer classBuffer) {
// 1
for (final field in visitor.fields.keys) {
  // 2
  final variable =
      field.startsWith('_') ? field.replaceFirst('_', '') : field;

  // 3 getter
  classBuffer.writeln(
      "${visitor.fields[field]} get $variable => variables['$variable'];");
  // EX: String get name => variables['name'];

  // 4 setter
  classBuffer.writeln(
      'set $variable(${visitor.fields[field]} $variable)');
  classBuffer.writeln('=> $field = $variable;');
  // EX: set name(String name) => _name = name;
  }
}

The code above writes getters and setters as extension methods.

  1. Again, you loop over all the variable names.
  2. Here, you remove _ from private variables.
  3. This writes the getter code.
  4. This writes the code for the setter.

This is just an alternative way to generate the model. Trying multiple approaches to solve a problem results in a better understanding of the concept.

Now that both generators are complete, it’s time to create builders from them.

Making Builders from the Generators

In build.yaml, you configured build_runner to look for builder.dart. So, as a last step, create builder.dart in lib and add this code:

// 1  
import 'package:build/build.dart';
// 2
import 'package:source_gen/source_gen.dart';

// 3
import 'src/extension_generator.dart';
import 'src/subclass_generator.dart';

// 4
Builder generateExtension(BuilderOptions options) =>
    SharedPartBuilder([ExtensionGenerator()], 'extension_generator');
Builder generateSubclass(BuilderOptions options) =>
    SharedPartBuilder([SubclassGenerator()], 'subclass_generator');

Here’s what the code does:

  1. You import build to get access to Builder. This base class is responsible for generating files from existing ones.
  2. source_gen provides some pre-implemented builders that cover common use cases of code generation. In this case, you need SharedPartBuilder, which renders part of files.
  3. Here, you import the generators you created above.
  4. These functions return the Builder for each of the two generators. SharedPartBuilder takes a list of generators as parameters to generate the code. To make each builder unique, you also need to provide an identifier. These functions are simple and apt for this use case, but you always have the power to configure the Builder more through BuilderOptions.
Note: part of is a directive of dart that allows you to access private variables or methods from another file.

Hurray! This completes both generators. Now, it’s time to test them out!

Testing the Generators

As mentioned before, you’ll use the example project to test the generated code. Open it in your preferred IDE and look at the dependencies in the project’s pubspec.yaml:

dependencies:
  annotations:
    path: ../annotations/

dev_dependencies:
  build_runner:
  generators:
    path: ../generators/

The file in the starter project already includes your annotations and generators, as well as the needed build_runner.

annotations is a dependency you’ll use during the compilation of the project. build_runner and generators are dev_dependencies because you only use them during the development process.

Get the dependencies by clicking the Get packages button, or however you do this in your IDE.

Now, create the model you’ll generate getters and setters for. Head over to lib and create profile_model.dart, like this:

// 1
import 'package:annotations/annotations.dart';

// 2
part 'profile_model.g.dart';

// 3
@generateSubclass
class ProfileModel { 
  // 4
  String _name = 'Aachman';
  int _age = 20;
  bool _codes = true;
}

Here’s what this code does:

  1. First, you import the annotations package.
  2. You add part 'profile_model.g.dart'; to include the generated file as a part of the original file.
  3. Using the annotation @generateSubclass, you trigger SubclassGenerator to generate code.
  4. Note that all fields are private. The generated code will make them public.

You can ignore the error message Target of URI hasn’t been generated: ‘profile_model.g.dart’. This is a typical error message when using generated code that you haven’t generated yet.

Generating the Code

Now, the moment you’ve been waiting for has come. It’s time to generate code!

Run this command in the terminal:

flutter pub run build_runner build

You’ll see something like this in the terminal console log:

[INFO] Generating build script...
[INFO] Generating build script completed, took 474ms

[INFO] Creating build script snapshot......
[INFO] Creating build script snapshot... completed, took 13.8s

[INFO] Initializing inputs
[INFO] Building new asset graph...
[INFO] Building new asset graph completed, took 793ms

[INFO] Checking for unexpected pre-existing outputs....
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms

[INFO] Running build...
[INFO] Generating SDK summary...
[INFO] 4.3s elapsed, 0/2 actions completed.
[INFO] Generating SDK summary completed, took 4.3s

[INFO] Running build completed, took 5.0s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 51ms

[INFO] Succeeded after 5.0s with 2 outputs (7 actions)

Congrats! You should see a new file named profile_model.g.dart generated in lib. Cross-check if it looks like this:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'profile_model.dart';

// **************************************************************************
// SubclassGenerator
// **************************************************************************

class ProfileModelGen extends ProfileModel {
  Map<String, dynamic> variables = {};
  ProfileModelGen() {
    variables['name'] = super._name;
    variables['age'] = super._age;
    variables['codes'] = super._codes;
  }
  String get name => variables['name'];
  set name(String name) {
    super._name = name;
    variables['name'] = name;
  }

  int get age => variables['age'];
  set age(int age) {
    super._age = age;
    variables['age'] = age;
  }

  bool get codes => variables['codes'];
  set codes(bool codes) {
    super._codes = codes;
    variables['codes'] = codes;
  }
}

OK, now go to main.dart and test out the generated model. First, import your model:

  import 'profile_model.dart'; 

Then, right under _ProfilePageState, add a line like this:

class _ProfilePageState extends State<ProfilePage> {
  ProfileModelGen profile = ProfileModelGen();
  ...
}

Here, you create an instance of ProfileModelGen that you want to test.

Now, search for the string ‘?’ in a Text widget and change it like this:

    child: Text(profile.name.substring(0, 1), 

Here, you use the generated getter for the name variable instead of '?'.

Here’s another test you can run: Check if the variables map contains all variables of the model. Search for the comment // TODO Display the values in the map and replace it, like so:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [ // TODO Display the values in the map
    // 1
    for (String key in profile.variables.keys.toList())
      RichText(
        text: TextSpan(children: [
          TextSpan(
            // 2
            text: '$key: '.toUpperCase(),
            style: TextStyle(
              fontSize: 24,
              color: Colors.grey[600],
            ),
          ),
          TextSpan(
            // 3
            text: '${profile.variables[key]}',
            style: TextStyle(
              fontSize: 36,
              color: Colors.green[200],
              fontWeight: FontWeight.bold,
              fontStyle: FontStyle.italic,
            ),
          ),
        ]),
      )
  ],
)

Here’s what the code above does:

  1. The for loop iterates over all the keys of the profile’s variables map.
  2. The first part of RichText displays the key, which is the name of the variable.
  3. The second part displays the stored value for the variable.

Build and run by entering flutter run in the terminal. The app will look like this:

The app showing the variables of the profile

Bravo! This means that SubclassGenerator works perfectly.

To test the second generator, just change the annotation to @generateExtension:

@generateExtension
class ProfileModel {
  String _name = 'Aachman';
  int _age = 20;
  bool _codes = true;
}

You need to generate the file again using the same command. However, since a generated file is already there, you need to delete it first. Don’t do that manually; instead, add –delete-conflicting-outputs to the command.

flutter pub run build_runner build --delete-conflicting-outputs

The console output is the same as for SubclassGenerator, but the newly generated file will look different:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'profile_model.dart';

// **************************************************************************
// ExtensionGenerator
// **************************************************************************

extension GeneratedModel on ProfileModel {
  Map<String, dynamic> get variables => {
        'name': _name,
        'age': _age,
        'codes': _codes,
      };
  String get name => variables['name'];
  set name(String name) => _name = name;
  int get age => variables['age'];
  set age(int age) => _age = age;
  bool get codes => variables['codes'];
  set codes(bool codes) => _codes = codes;
}

In main.dart, replace ProfileModelGen with ProfileModel and nothing else:

ProfileModel profile = ProfileModel();

Build and run! Everything should still work the same.

You did it! You just built a code generation library from scratch. Feel free to explore other APIs offered by source_gen and build to create even more powerful code generation tools.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial. Now, you know how to create a code generator. Even if you never need to build your own, it’s good to understand how they work under the hood.

If you want to learn more about code generation, here are a few useful resources:

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

3 ratings

More like this

Contributors

Comments