Creating Custom Code Generators in Flutter

Flutter’s declarative UI framework and rich ecosystem make it a fantastic choice for building cross-platform applications. However, as your project grows in complexity, tasks such as generating boilerplate code can become tedious and error-prone. This is where custom code generators come into play. Creating custom code generators in Flutter can significantly improve developer productivity by automating repetitive coding tasks.

What is a Code Generator?

A code generator is a tool that automatically generates source code based on a predefined set of rules or templates. In the context of Flutter, code generators can automate the creation of models, serializers, UI components, and other boilerplate code. This reduces the amount of manual coding required, minimizes errors, and ensures consistency across your codebase.

Why Use Code Generators?

  • Reduce Boilerplate: Automatically generate repetitive code, such as data models and serialization logic.
  • Improve Consistency: Ensure uniform code patterns throughout the project.
  • Enhance Productivity: Save development time by automating routine tasks.
  • Minimize Errors: Reduce the risk of manual coding errors.

How to Create Custom Code Generators in Flutter

To create a custom code generator in Flutter, you’ll typically use Dart’s build system. The build system allows you to define builders that process input files and generate corresponding output files. Here’s a step-by-step guide to creating a simple code generator.

Step 1: Set Up a New Flutter Project

If you don’t already have a Flutter project, create a new one using the Flutter CLI:


flutter create custom_code_generator
cd custom_code_generator

Step 2: Add Dependencies

Add the necessary dependencies to your pubspec.yaml file. You’ll need build_runner, build_config, and any packages that help with code generation like source_gen and analyzer.


dependencies:
  flutter:
    sdk: flutter
  # Add other dependencies as needed

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.6
  build_config: ^2.2.0
  source_gen: ^1.5.0
  analyzer: '>=4.0.0 <6.0.0'

flutter:
  uses-material-design: true

Run flutter pub get to install the dependencies.

Step 3: Define Your Builder

Create a file named build.yaml in the root of your project to define the builder. This file tells the build system how to run your code generator.


targets:
  $default:
    builders:
      custom_code_generator:
        enabled: true

builders:
  custom_code_generator:
    target: ":custom_code_generator"
    sources:
      - "lib/src/models/*.dart"
    builder_factories: ["CustomGeneratorBuilder"]
    import: "package:custom_code_generator/builder.dart"
    build_extensions: {".dart": [".g.dart"]}
    auto_apply: dependents
    

Explanation of the build.yaml file:

  • targets: Specifies the target to apply the builder to.
  • builders: Defines the builder configuration.
  • target: Specifies the files that the builder will process.
  • sources: Lists the input files. Here, we’re looking for Dart files in lib/src/models/.
  • builder_factories: Specifies the function that creates the builder.
  • import: The path to the file containing the builder factory function.
  • build_extensions: Defines the output file extension. The generator will produce files with a .g.dart extension.
  • auto_apply: Ensures that the builder is automatically applied to dependents.

Step 4: Implement the Builder

Create the builder.dart file, which contains the actual code generator logic. This file will define the CustomGeneratorBuilder class.


import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

Builder CustomGeneratorBuilder(BuilderOptions options) =>
    PartBuilder([CustomGenerator()], '.g.dart');

class CustomGenerator extends Generator {
  @override
  Future generate(LibraryElement library, BuildStep buildStep) async {
    final buffer = StringBuffer();

    for (final compilationUnit in library.units) {
      for (final element in compilationUnit.topLevelElements) {
        if (element is ClassElement) {
          final className = element.name;
          buffer.writeln('class $$className {');
          buffer.writeln('  String get type => '$className';');
          buffer.writeln('}');
        }
      }
    }

    return buffer.toString();
  }
}

In this code:

  • We import necessary packages from build, source_gen, and analyzer.
  • CustomGeneratorBuilder is a factory function that returns a PartBuilder, which links our CustomGenerator to the build system.
  • CustomGenerator extends the Generator class and overrides the generate method. This method is called for each library in your project.
  • Inside the generate method, we iterate through the elements of the library, looking for classes.
  • For each class found, we generate a new class named $className with a type getter that returns the original class name.

Step 5: Create Input Files

Create the input files that the code generator will process. These files should be placed in the directory specified in the build.yaml file (i.e., lib/src/models/).


// lib/src/models/user.dart
class User {
  String name;
  int age;

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

// lib/src/models/address.dart
class Address {
  String street;
  String city;

  Address({required this.street, required this.city});
}

Step 6: Run the Code Generator

Run the code generator using the following command:


flutter pub run build_runner build

This command starts the build runner, which will execute your custom code generator. After the build process completes, you’ll find generated files (e.g., user.g.dart and address.g.dart) in the same directory as the input files.

Step 7: Use the Generated Code

Import the generated files into your Dart code and use the generated classes.


// lib/main.dart
import 'package:flutter/material.dart';
import 'package:custom_code_generator/src/models/user.dart';
import 'package:custom_code_generator/src/models/user.g.dart';
import 'package:custom_code_generator/src/models/address.dart';
import 'package:custom_code_generator/src/models/address.g.dart';

void main() {
  final user = User(name: 'John Doe', age: 30);
  final $user = $User();

  final address = Address(street: '123 Main St', city: 'Anytown');
  final $address = $Address();

  print('User type: ${$user.type}');    // Output: User type: User
  print('Address type: ${$address.type}');  // Output: Address type: Address

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Code Generator Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Custom Code Generator Demo'),
        ),
        body: Center(
          child: Text('Check the console for generated code output!'),
        ),
      ),
    );
  }
}

This example demonstrates how to use the generated code. It creates instances of the original classes and their generated counterparts, then prints the type of each class.

Advanced Techniques

To create more sophisticated code generators, consider the following techniques:

  • Use Annotations: Define custom annotations to control the code generation process. This allows you to specify which classes or fields should be processed.
  • Handle Complex Logic: Implement more complex logic within your generator to handle different scenarios and generate tailored code.
  • Integrate with Other Tools: Integrate your code generator with other development tools, such as linters and formatters, to ensure code quality.

Example: Using Annotations

Add an annotation to specify which classes should be processed.


// lib/src/annotations.dart
class GenerateType {
  const GenerateType();
}

Modify your model files to include the annotation.


// lib/src/models/user.dart
import 'package:custom_code_generator/src/annotations.dart';

@GenerateType()
class User {
  String name;
  int age;

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

Update your code generator to check for the annotation.


// lib/builder.dart
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:custom_code_generator/src/annotations.dart';
import 'package:source_gen/source_gen.dart';

Builder CustomGeneratorBuilder(BuilderOptions options) =>
    PartBuilder([CustomGenerator()], '.g.dart');

class CustomGenerator extends Generator {
  @override
  Future generate(LibraryElement library, BuildStep buildStep) async {
    final buffer = StringBuffer();

    for (final compilationUnit in library.units) {
      for (final element in compilationUnit.topLevelElements) {
        if (element is ClassElement) {
          final annotation = TypeChecker.fromRuntime(GenerateType)
              .firstAnnotationOfExact(element);

          if (annotation != null) {
            final className = element.name;
            buffer.writeln('class $$className {');
            buffer.writeln('  String get type => '$className';');
            buffer.writeln('}');
          }
        }
      }
    }

    return buffer.toString();
  }
}

In this modified generator, the generate method now checks for the @GenerateType() annotation before generating any code. Only classes with this annotation will have generated counterparts.

Conclusion

Creating custom code generators in Flutter is a powerful way to automate repetitive coding tasks, improve consistency, and enhance developer productivity. By leveraging Dart’s build system and code analysis tools, you can create sophisticated generators that tailor code to your specific needs. Whether you’re generating data models, serializers, or UI components, code generators can significantly streamline your Flutter development workflow.