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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.