Flutter Code Generation: Getting Started
Learn how to use code generation to automatically create Dart models, eliminating tedious and repetitive tasks. By Aachman Garg.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter Code Generation: Getting Started
25 mins
- Getting Started
- Why Code Generation?
- Setting up Your Initial Project
- The Annotations Folder
- The Generators Folder
- Build.yaml
- The Example Folder
- Creating the Necessary Annotations
- Creating the Annotation Classes
- Creating the Generators
- Finding Annotated Classes With ModelVisitor
- Implementing a Generator for a Subclass
- Implementing a Generator for an Extension
- Making Builders from the Generators
- Testing the Generators
- Generating the Code
- Where to Go From Here?
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
.
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:
- You create the class,
ModelVisitor
, that extendsSimpleElementVisitor
.SimpleElementVisitor
has most of the methods you need already implemented. - 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 askey
and its datatype asvalue
. You’ll need both to generate getters and setters. - You override
visitConstructorElement
to obtain theclassName
by accessingtype.returnType
of each found constructor. -
elementReturnType
ends with *, which you need to remove for the generated code to be accurate. -
visitFieldElement
fillsfields
with the names and datatypes of all the variables found in the target class. - 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:
- You override
generateForAnnotatedElement
, which takes anelement
. In this case, thatelement
is a class. You don’t need the other parameters in this simple case. The returnedString
contains the generated code. - Start by visiting the class’s children.
- Then, create
classname
for the generated class. - Because you need to work with a lot of
String
s, using aStringBuffer
is a good option. - This is the point where you start writing the generated code lines. Create the class that extends the target class.
- Next, create the
variables
map that will store all of target class’s variables. - Add the constructor of the class.
- Assign the target class’s variables to the map.
field
represents the variable’s name. - End the constructor body.
- Call
generateGettersAndSetters
— well, to generate the getters and setters of all the variables. - Close the constructor.
- 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:
- You loop over all variable names.
- Here, you remove _ from the private variables of the base class.
- This writes the getter code.
visitor.fields[field]
represents the variable’s datatype. - 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:
- Import the necessary packages.
- Create the class with
ExtensionAnnotation
as a generic type parameter. - Implement the same method as you did in
SubclassGenerator
. - Visit the class children and initialize
StringBuffer
. - Here comes the difference! Start the extension
GeneratedModel
. - Start the getter for the
variables
map. - Add entries to the
variables
map. - End the getter for the map.
- Again call
generateGettersAndSetters
. - This ends the definition of the extension.
- 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.
- Again, you loop over all the variable names.
- Here, you remove _ from private variables.
- This writes the getter code.
- 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.