Code generation
Last modified on Tue 09 Nov 2021

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:

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.

Using build_runner

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:

// 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

The build package

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:

// 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');
  }

The source_gen package

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