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 3 of 4 of this article. Click here to view the first page.

Making Builders from the Generators

In build.yaml, you configured build_runner to look for builder.dart. So, as a last step, create builder.dart in lib and add this code:

// 1  
import 'package:build/build.dart';
// 2
import 'package:source_gen/source_gen.dart';

// 3
import 'src/extension_generator.dart';
import 'src/subclass_generator.dart';

// 4
Builder generateExtension(BuilderOptions options) =>
    SharedPartBuilder([ExtensionGenerator()], 'extension_generator');
Builder generateSubclass(BuilderOptions options) =>
    SharedPartBuilder([SubclassGenerator()], 'subclass_generator');

Here’s what the code does:

  1. You import build to get access to Builder. This base class is responsible for generating files from existing ones.
  2. source_gen provides some pre-implemented builders that cover common use cases of code generation. In this case, you need SharedPartBuilder, which renders part of files.
  3. Here, you import the generators you created above.
  4. These functions return the Builder for each of the two generators. SharedPartBuilder takes a list of generators as parameters to generate the code. To make each builder unique, you also need to provide an identifier. These functions are simple and apt for this use case, but you always have the power to configure the Builder more through BuilderOptions.
Note: part of is a directive of dart that allows you to access private variables or methods from another file.

Hurray! This completes both generators. Now, it’s time to test them out!

Testing the Generators

As mentioned before, you’ll use the example project to test the generated code. Open it in your preferred IDE and look at the dependencies in the project’s pubspec.yaml:

dependencies:
  annotations:
    path: ../annotations/

dev_dependencies:
  build_runner:
  generators:
    path: ../generators/

The file in the starter project already includes your annotations and generators, as well as the needed build_runner.

annotations is a dependency you’ll use during the compilation of the project. build_runner and generators are dev_dependencies because you only use them during the development process.

Get the dependencies by clicking the Get packages button, or however you do this in your IDE.

Now, create the model you’ll generate getters and setters for. Head over to lib and create profile_model.dart, like this:

// 1
import 'package:annotations/annotations.dart';

// 2
part 'profile_model.g.dart';

// 3
@generateSubclass
class ProfileModel { 
  // 4
  String _name = 'Aachman';
  int _age = 20;
  bool _codes = true;
}

Here’s what this code does:

  1. First, you import the annotations package.
  2. You add part 'profile_model.g.dart'; to include the generated file as a part of the original file.
  3. Using the annotation @generateSubclass, you trigger SubclassGenerator to generate code.
  4. Note that all fields are private. The generated code will make them public.

You can ignore the error message Target of URI hasn’t been generated: ‘profile_model.g.dart’. This is a typical error message when using generated code that you haven’t generated yet.

Generating the Code

Now, the moment you’ve been waiting for has come. It’s time to generate code!

Run this command in the terminal:

flutter pub run build_runner build

You’ll see something like this in the terminal console log:

[INFO] Generating build script...
[INFO] Generating build script completed, took 474ms

[INFO] Creating build script snapshot......
[INFO] Creating build script snapshot... completed, took 13.8s

[INFO] Initializing inputs
[INFO] Building new asset graph...
[INFO] Building new asset graph completed, took 793ms

[INFO] Checking for unexpected pre-existing outputs....
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms

[INFO] Running build...
[INFO] Generating SDK summary...
[INFO] 4.3s elapsed, 0/2 actions completed.
[INFO] Generating SDK summary completed, took 4.3s

[INFO] Running build completed, took 5.0s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 51ms

[INFO] Succeeded after 5.0s with 2 outputs (7 actions)

Congrats! You should see a new file named profile_model.g.dart generated in lib. Cross-check if it looks like this:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'profile_model.dart';

// **************************************************************************
// SubclassGenerator
// **************************************************************************

class ProfileModelGen extends ProfileModel {
  Map<String, dynamic> variables = {};
  ProfileModelGen() {
    variables['name'] = super._name;
    variables['age'] = super._age;
    variables['codes'] = super._codes;
  }
  String get name => variables['name'];
  set name(String name) {
    super._name = name;
    variables['name'] = name;
  }

  int get age => variables['age'];
  set age(int age) {
    super._age = age;
    variables['age'] = age;
  }

  bool get codes => variables['codes'];
  set codes(bool codes) {
    super._codes = codes;
    variables['codes'] = codes;
  }
}

OK, now go to main.dart and test out the generated model. First, import your model:

  import 'profile_model.dart'; 

Then, right under _ProfilePageState, add a line like this:

class _ProfilePageState extends State<ProfilePage> {
  ProfileModelGen profile = ProfileModelGen();
  ...
}

Here, you create an instance of ProfileModelGen that you want to test.

Now, search for the string ‘?’ in a Text widget and change it like this:

    child: Text(profile.name.substring(0, 1), 

Here, you use the generated getter for the name variable instead of '?'.

Here’s another test you can run: Check if the variables map contains all variables of the model. Search for the comment // TODO Display the values in the map and replace it, like so:

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [ // TODO Display the values in the map
    // 1
    for (String key in profile.variables.keys.toList())
      RichText(
        text: TextSpan(children: [
          TextSpan(
            // 2
            text: '$key: '.toUpperCase(),
            style: TextStyle(
              fontSize: 24,
              color: Colors.grey[600],
            ),
          ),
          TextSpan(
            // 3
            text: '${profile.variables[key]}',
            style: TextStyle(
              fontSize: 36,
              color: Colors.green[200],
              fontWeight: FontWeight.bold,
              fontStyle: FontStyle.italic,
            ),
          ),
        ]),
      )
  ],
)

Here’s what the code above does:

  1. The for loop iterates over all the keys of the profile’s variables map.
  2. The first part of RichText displays the key, which is the name of the variable.
  3. The second part displays the stored value for the variable.

Build and run by entering flutter run in the terminal. The app will look like this:

The app showing the variables of the profile

Bravo! This means that SubclassGenerator works perfectly.

To test the second generator, just change the annotation to @generateExtension:

@generateExtension
class ProfileModel {
  String _name = 'Aachman';
  int _age = 20;
  bool _codes = true;
}

You need to generate the file again using the same command. However, since a generated file is already there, you need to delete it first. Don’t do that manually; instead, add –delete-conflicting-outputs to the command.

flutter pub run build_runner build --delete-conflicting-outputs

The console output is the same as for SubclassGenerator, but the newly generated file will look different:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'profile_model.dart';

// **************************************************************************
// ExtensionGenerator
// **************************************************************************

extension GeneratedModel on ProfileModel {
  Map<String, dynamic> get variables => {
        'name': _name,
        'age': _age,
        'codes': _codes,
      };
  String get name => variables['name'];
  set name(String name) => _name = name;
  int get age => variables['age'];
  set age(int age) => _age = age;
  bool get codes => variables['codes'];
  set codes(bool codes) => _codes = codes;
}

In main.dart, replace ProfileModelGen with ProfileModel and nothing else:

ProfileModel profile = ProfileModel();

Build and run! Everything should still work the same.

You did it! You just built a code generation library from scratch. Feel free to explore other APIs offered by source_gen and build to create even more powerful code generation tools.