Unleashing the Sorcery: Crafting a Code Generator with Flutter & Dart

Furkan Acar
8 min readNov 1, 2023

--

Created by Adobe Firefly

In the busy world of making software, we want things to be quick and the same every time. Have you ever felt tired of writing the same code again and again? Or wish you didn’t make small mistakes? This is where code generation helps. Here’s why it’s changing how we make software:”

  1. Making Data Classes Easier: Think about the basic classes you use in your projects. These classes add up fast. Code generation helps make these classes for you, saving you time and effort. ex: chopper,retrofit
  2. No More Repeating Code: Have you ever been tired of writing the same type of code again and again? Some coding patterns make you repeat a lot. But tools, like riverpod, mobx help you focus on the main work and not on writing the same structure.
  3. Adding Common Features: We often use the same functions in many classes, like turning data into objects or copying objects. Instead of writing these every time, code generators can add them quickly to any class. ex: json_serializable, freezed, riverpod_feature_generator (I coded the last one. It is ultra beta.)
  4. Better Code Quality: Using code generation means you write less, and the code you get is the same quality every time because the generator writes the codes for you every time. If you fix the error only for the generator. You can correct it anywhere.

In short, code generation is like having a helpful tool that does the boring stuff, so you can work on the hard stuff. It saves time, works well, and is a great tool for today’s coders.

LET’S START
Photo by Clemens van Lay on Unsplash

Let’s build “fromJson” and “toJson” generator

If you have used a generator before, you know that you need to put an annotation in the class that the generator wants to run. Then let’s write an annotation.

ANNOTATION

class SimpleAnnotation {
const SimpleAnnotation();
}

const simple = SimpleAnnotation();

This is the simple annotation class. You can call it with @simple or @SimpleAnnotation(). Also, you can add fields to annotation.

class SimpleAnnotation {
final bool addHello;

const SimpleAnnotation({this.addHello = false});
}

const simple = SimpleAnnotation();

In this way, you can get the inputs you want from the user and direct the generation process according to these inputs. You can use it with @SimpleAnnotation(addHello: true) or @simple.

That’s all for the annotation part. If you want to do it in a different module like me, you can create a new module by running the flutter create --template=package my_annotation_name command from the terminal and adding the codes we wrote under the module.

Let’s move on to the generator part. Before you start, if you want to make it modular like me. You can create a new module with flutter create --template=package my_generator_name .

name: generator
description: "A simple generator project."
version: 0.0.1
homepage:
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
flutter:
sdk: flutter
build:
source_gen:
graphs: 2.2.0
annotation:
path: ../annotation
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner:

flutter:

We did not add any packages to annotation, but we need to add them to generation. graphs: package is an optional package, you may be getting errors depending on your version. If you try this graph version, your error will most likely be resolved.

VISITOR

We need to use ElementVisitorto read the class we marked with annotation. We have ElementVisitor with different functionality. We can use SimpleElementVisitor, where we can simply visit elements.

class ModelVisitor extends SimpleElementVisitor<void> {}

There are several useful methods that we can override from SimpleElementVisitor. But we can start by implementing the two that worked for us: visitConstructorElement, and visitFieldElement.

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

class ModelVisitor extends SimpleElementVisitor<void> {
String className = '';
Map<String, dynamic> fields = {};

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

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

We get the class name with visitConstructorElement, and we get our fields with visitFieldElement. After reading these fields, we need to make a generation with these fields. let’s get to that part.

GENERATOR

class SimpleGenerator extends GeneratorForAnnotation<SimpleAnnotation>{}

When writing the generator, you need to create a class that extends GeneratorForAnnotation and assign the annotation type to GeneratorForAnnotation. For example, since our annotation is SimpleAnnotation, we will extend it as GeneratorForAnnotation<SimpleAnnotation>.

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

class SimpleGenerator extends GeneratorForAnnotation<SimpleAnnotation> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
// TODO: implement generateForAnnotatedElement
throw UnimplementedError();
}
}

Since GeneratorForAnnotation is an abstract class and the body of the generateForAnnotatedElement method is not defined, you need to define this method.

Let’s examine the method

Element — This represents the Dart element being processed (e.g. a class, function or variable).

ConstantReader — This helps us read the fields values of the annotation on the rendered element.

BuildStep — This allows us to perform various tasks during the compilation process.

import 'package:analyzer/dart/element/element.dart';
import 'package:annotation/annotation.dart';
import 'package:build/build.dart';
import 'package:generator/src/model_visitor.dart';
import 'package:source_gen/source_gen.dart';

class SimpleGenerator extends GeneratorForAnnotation<SimpleAnnotation> {
@override
String generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
//1
final visitor = ModelVisitor();
element.visitChildren(visitor);
//2
bool addHello = annotation.read('addHello').boolValue;
//3
String className = visitor.className;
Map<String, dynamic> fields = visitor.fields;

//4
final toJsonCode = _generateToJsonCode(className, fields);
final fromJsonCode = _generateFromJsonCode(className, fields);
//5
final printHelloCode = addHello ? addHelloComment() : '';
//6
return '$toJsonCode\n\n$fromJsonCode\n\n$printHelloCode';
}

String _generateToJsonCode(String className, Map<String, dynamic> fields) {
final fieldMapEntries =
fields.keys.map((key) => " '$key': $key").join(',\n');

return '''
extension ${className}ToJson on $className {
Map<String, dynamic> toJson() => {
$fieldMapEntries
};
}
''';
}

String _generateFromJsonCode(String className, Map<String, dynamic> fields) {
final fieldAssignments =
fields.keys.map((key) => "$key: this['$key']").join(',\n');

return '''
extension ${className}FromJson on Map<String, dynamic> {
$className to$className() => $className(
$fieldAssignments
);
}
''';
}

String addHelloComment() {
return '// Hello from SimpleGenerator';
}
}

1- Visits all the children of the element in no particular order.

2- Reads addhello field from annotation,

3- takes className and fields from visitor,

4- Generates a “tojson” and “fromjson” code snippet based on classname and field.

5- It calls the addhello method depending on whether addhello is true or not.

6- Here, when the code generator runs, we return the code we want to write to the newly created file.

The code part is almost over, now we will add how to trigger the codes we wrote.

library generator;

import 'package:build/build.dart';
import 'package:generator/src/simple_generator.dart';
import 'package:source_gen/source_gen.dart';

Builder generateSimpleGenClass(BuilderOptions options) => SharedPartBuilder(
[SimpleGenerator()],
'simple_generator',
);
  1. Builder:
  • In the context of Dart and Flutter source generation, a Builder is a fundamental component that defines how to generate additional source code based on existing code.
  • Think of it as a “machine” that takes some Dart code as input, processes it, and produces new Dart code as output.

2. SharedPartBuilder:

  • This is a specific type of Builder provided by the source_gen package.
  • Its primary purpose is to allow multiple generators to contribute to a single output file. This way, if you have multiple generators that want to produce code for a single Dart file, they can all contribute their generated code to one shared file.
  • The output file will have a .g.dart extension by default, but the SharedPartBuilder allows you to specify an additional identifier (like 'simple_generator') to differentiate between different shared parts.

In simpler terms:

  • A Builder is like a "recipe" that tells how to make a new piece of code based on some existing code.
  • A SharedPartBuilder is a special "recipe" that lets multiple "chefs" (generators) add their own "ingredients" (generated code) to a single "dish" (output file).
  • When you generate new files, the extension of these files will not be simple_generator, but when you receive an error, you can see simple_generator in the file extension.
  1. targets:
  • This section says which Dart files should be processed by which builders.
  • $default means the default target, usually all Dart files in your project.
  • Inside $default, you enable a builder called generators|annotations. But this builder name isn't defined in your file, which might be a mistake.
targets:
$default:
builders:
generators|annotations:
enabled: true

builders:
generators:
target: ":generator"
import: "package:generator/generator.dart"
builder_factories: ["generateSimpleGenClass"] #Builder name(BuilderOptions)
build_extensions: { ".dart": [".g.dart"] }
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]


  1. targets:
  • This section says which Dart files should be processed by which builders.
  • $default means the default target, usually all Dart files in your project.
  • Inside $default, you enable a builder called generators|annotations.

2. builders:

  • Here, you define your custom builders.
  • generators: The name of your builder.
  • target: Which target this builder is for. :generator is a custom target we defined.
  • import: The Dart file where our builder is defined.
  • builder_factories: The functions or classes our builder uses. Here, we use generateSimpleGenClass.
  • build_extensions: Tells the builder to process .dart files and create new files with .g.dart extension.
  • auto_apply: Automatically apply this builder to dependent packages.
  • build_to: Where the generated files are saved. cache means Flutter's cache folder.
  • applies_builders: Other builders this builder will use. Here, it combines code from different generators into one file.

Generation is also okay, let’s make an example

Let’s create a flutter project named “example”

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
annotation:
path: ../annotation

dev_dependencies:
flutter_test:
sdk: flutter


flutter_lints: ^3.0.0
build_runner: 2.3.3
generator:
path: ../generator

We need to add “annotation” under dependencies and add the “generator” under dev_dependency.

import 'package:annotation/annotation.dart';

part 'user.g.dart';

@simple
class User {
final String name;
final int age;

User({required this.name, required this.age});
}

Let’s create a user model and add @simple annotation and part path to the class.

After these steps, if you run this command dart run build_runner build — delete-conflicting-outputs in the terminal path of the project folder, our file will be created.

If we replace the @simple code with the @SimpleAnnotation(addHello: true) code and run the necessary code from the terminal again, we will see that the hello comment has been added this time.

Source Code:

Photo by Joshua Hoehne on Unsplash

This was an example of how you can make a simple generator, I hope you liked it and it will be useful to you. Thank you for reading :)

--

--