Code generation is a way to automate things, to let the computer do the work for us. That being said, code generation does introduce additional steps to the development process. Before using it, try to find a way to avoid it.
Using code generation
There are two main parts when using code generation:
builder
a piece of code that does the code generationbuild system
the thing that runs thebuilder
For example, when using code generation for JSON serialization/deserialization, the json_serializable package provides the builder
, and we use the build_runner build system
to activate the builder
.
In practice, we usually use code generation for generating Dart source code, although it can work with and produce any type of file. As the build system
, we use a standalone build system
the build_runner, other build systems
include pub and bazel.
build_runner
Using build_runner is one of the Dart build systems
(a tool that invokes the code-generating code).
We add it as a dev_dependency
and run it from the command line.
Use the build
command to generate the code once, or use the watch
command to let the build_runner
automatically do its work when something changes.
It can be useful to add the --delete-conflicting-outputs
argument, it will automatically delete the previously generated files if they aren't needed anymore.
// start code generation with `build_runner`
pub run build_runner build
// when using Flutter
flutter pub run build_runner build
Using source code generation packages
One way of using code generation is to generate Dart source code. With the help of annotations, the computer can generate the repetitive and boilerplate code.
By convention, the generated source code files have a .g.dart
extension (a notable exception are files generated with the freezed package). Generated code is imported with the part
keyword.
Source code generation packages can usually be divided into two parts:
annotation
contains the code annotations that indicate to thegenerator
what to dogenerator
contains code that will generate other code,generator
is added as adev_dependency
// Generating JSON serialization/deserialization source code with the `json_serializable` package
// if this file is named `user.dart`, the generated file will be named `user.g.dart`
import 'package:json_annotation/json_annotation.dart'; // importing the `annotation`
part `user.g.dart`; // importing the generated code
@JsonSerializable() // using annotation, to tell the `generator` to generate serealization code for this class
class User {
User(this.firstName, this.lastName);
final String firstName;
final String lastName;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); // using the generated code (`_$UserFromJson`)
Map<String, dynamic> toJson() => _$UserToJson(this);
}
To avoid the linter complaining, we can exclude the generated files by modifying the analysis_options.yaml
file.
analyzer:
exclude:
- 'lib/**/*.freezed.dart'
- 'lib/**/*.g.dart'
Custom code generation
build.yaml
file
If code generation will be done with build_runner
, we must create a build.yaml
file. It contains the configuration. Among other things, it tells the build_runner
where the code-generating code is located, and what type of files will it be producing (documentation).
# `build.yaml` file for a `builder` that will create some statistics about the code
builders:
statistics_builder:
import: "package:build_generator/builder.dart"
builder_factories: ["statisticsBuilder"]
build_extensions: {"$package$": [".md"]}
auto_apply: dependents
build_to: source
build
package
The Since there are multiple build systems
, there needs to be a standard way to create a builder
. Every builder
must implement the Builder
interface from the build package.
Builder
has two overrides:
buildExtensions
returns what types of files will the builder be usingbuild
method does the code generation.
// Using code generation to produce some statistics
// `build_runner` calls this method to obtain the builder (configured in `build.yaml`)
Builder statisticsBuilder(BuilderOptions options) {
return StatisticsBuilder(options);
}
// every `builder` must implement the `Builder` interface
class StatisticsBuilder implements Builder {
StatisticsBuilder(this.builderOptions);
final BuilderOptions builderOptions;
@override
Map<String, List<String>> get buildExtensions => {
r'$package$': ['statistics.md'],
};
@override
FutureOr<void> build(BuildStep buildStep) async {
final outputId = AssetId(buildStep.inputId.package, 'statistics.md');
var lineCount = 0;
final sourceCodeAssets = buildStep.findAssets(Glob('**/*.dart'));
await for (final asset in sourceCodeAssets) {
lineCount += (await buildStep.readAsString(asset)).split('\n').length;
}
await buildStep.writeAsString(outputId, 'Line count: $lineCount');
}
source_gen
package
The The source_gen package is an extension to the build package. It contains a set of utilities that make generating Dart source code easier. For example, we use the GeneratorForAnnotation
to find annotations and generate code based on them.
// Using `source_gen` to generate source code for an extension that adds a `copyWith` method
// `build_runner` calls this method to obtain the builder (configured in `build.yaml`)
Builder copyWith(BuilderOptions options) {
// `SharedPartBuilder` is a convenience class from the `source_gen` package,
// it implements `Builder` and adds the `part of` declaration to the generated files
return SharedPartBuilder([CopyWithGenerator()], 'copyWith');
}
// when using `source_gen`, the generator is the class that generates the source code
class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
// this function will be run for every `CopyWith` annotation in the code
// result of this function is the generated code
@override
String generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
// analyzing the code, to know what to generate
final visitor = MyVisitor();
element.visitChildren(visitor);
// generating the code
final buffer = StringBuffer();
buffer.writeln('extension ${visitor.className}CopyWithExtension on ${visitor.className} {');
buffer.writeln('${visitor.className} copyWith({');
// ...
return buffer.toString();
}
}
Other useful packages
The analyzer package, provides static analysis on Dart code. We can use it to find the class name or its variables.
// Using the help of the `analyzer` package, to find the class name
class MyVisitor extends SimpleElementVisitor<void> {
late final String className;
@override
void visitConstructorElement(ConstructorElement element) {
className = element.type.returnType.toString();
}
}
To produce the actual lines of code we can combine strings or use the code_builder package.
// Creating Dart code with Dart code
final generated = Class((c) => c
..name = 'MyClass'
..methods.add(
Method((m) => m
..name = 'myMethod'
..body = Code('...')),
));
final emitter = DartEmitter(useNullSafetySyntax: true);
final code = DartFormatter().format(generated.accept(emitter).toString());
Conventions
- Commit the generated files to the source tree
- Never manually edit the generated files
- Generated source code files should have a
.g.dart
extension (except when using freezed) - Exclude the generated files in the
analysis_options.yaml