Flutter’s ecosystem is rich with tools and techniques to enhance productivity and maintainability. One powerful feature is code generation, which allows you to automate the creation of repetitive or boilerplate code. By creating custom code generation scripts, you can streamline your development process, reduce errors, and enforce consistency across your Flutter projects.
What is Code Generation?
Code generation is the process of automatically creating source code based on a defined template or schema. It’s particularly useful for reducing boilerplate, ensuring consistency, and automating repetitive tasks. In Flutter, code generation is often used to create serializers, data models, and other infrastructure code.
Why Use Custom Code Generation Scripts?
- Reduce Boilerplate: Automatically generate repetitive code, saving development time.
- Ensure Consistency: Enforce coding standards and patterns across the project.
- Improve Maintainability: Update code generation templates to propagate changes across multiple files.
- Automate Complex Tasks: Handle complex code transformations or generation processes.
How to Create Custom Code Generation Scripts in Flutter
Creating custom code generation scripts in Flutter typically involves using tools like build_runner, analyzer, and custom scripts. Here’s a step-by-step guide:
Step 1: Set Up Your Flutter Project
Create a new Flutter project or navigate to an existing one.
flutter create my_code_gen_app
cd my_code_gen_app
Step 2: Add Dependencies
Add the necessary dependencies to your pubspec.yaml file:
dependencies:
build_runner: ^2.4.6
analyzer: ^6.3.0
source_gen: ^1.4.1
build: ^2.4.1
dev_dependencies:
build_verify: ^3.1.0
Explanation of Dependencies:
build_runner: A tool for running code generators during development.analyzer: Provides APIs for static analysis of Dart code.source_gen: Provides utilities for creating source code generators.build: Base package for defining build systems and build stepsbuild_verify:Ensures that the generated code matches the expected output
Run flutter pub get to install the dependencies.
Step 3: Create a Custom Annotation
Define a custom annotation that will be used to mark the classes or members that should be processed by the code generator. Create a new file named my_annotation.dart in the lib directory:
class MyAnnotation {
const MyAnnotation();
}
const myAnnotation = MyAnnotation();
Step 4: Implement a Custom Generator
Create a custom generator that will read the annotated elements and generate the desired code. Create a new file named my_generator.dart in the lib directory:
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'my_annotation.dart';
class MyGenerator extends GeneratorForAnnotation {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (element is ClassElement) {
final className = element.name;
return '''
class $${className}Helper {
String getHelperText() => 'Hello from $${className}Helper for class $className';
}
''';
} else {
throw UnsupportedError(
'MyAnnotation can only be applied to classes. Failed on $element');
}
}
}
Explanation:
MyGenerator: ExtendsGeneratorForAnnotationto process elements annotated withMyAnnotation.generateForAnnotatedElement: This method is called for each annotated element. It checks if the element is a class, extracts the class name, and generates a helper class.- The generated helper class (
$${className}Helper) contains a simple method that returns a text message.
Step 5: Create a Builder Definition
Create a builder definition to integrate the custom generator with build_runner. Create a new file named build.yaml in the root of your project:
targets:
$default:
sources:
- lib/**
builders:
my_code_gen_app:my_generator:
options:
some_option: true
Explanation:
targets: Defines which source files should be processed.lib/**includes all files in thelibdirectory.builders: Defines the builder to use.my_code_gen_app:my_generator: Refers to the builder in your project (my_code_gen_appis the package name).options: Allows passing options to the generator.
Step 6: Apply the Annotation
Apply the @MyAnnotation annotation to the classes that you want to process with the code generator. Create or modify a Dart file in the lib directory (e.g., lib/my_class.dart):
import 'my_annotation.dart';
@myAnnotation
class MyClass {
String myProperty = 'Hello';
}
Step 7: Run the Code Generator
Run the code generator using build_runner:
flutter pub run build_runner build --delete-conflicting-outputs
This command will:
- Scan your project for files annotated with
@MyAnnotation. - Run the
MyGeneratorto generate the corresponding helper classes. - Create a new file (e.g.,
lib/my_class.g.dart) containing the generated code.
Step 8: Use the Generated Code
Import and use the generated code in your Flutter application. In your lib/main.dart or any other file, add:
import 'package:flutter/material.dart';
import 'my_class.dart';
import 'my_class.g.dart'; // Import the generated file
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myClass = MyClass();
final helper = $MyClassHelper(); // Use the generated helper class
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Code Generation Example'),
),
body: Center(
child: Text(helper.getHelperText()), // Access the generated method
),
),
);
}
}
Step 9: Verify the generated code
This step validates the generated code after each build. This prevents the check-in of generated code that doesn’t match what you’d get from a clean build.
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run build_verify
Example build_verify Workflow Action
Alternatively, you can use it directly in a GitHub Actions workflow:
name: build_verify
on:
push:
branches:
- main
pull_request:
jobs:
build_verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dart-lang/setup-dart@v1
- run: dart pub get
- run: dart pub global activate build_runner
- run: dart run build_runner build --delete-conflicting-outputs
- run: dart pub global activate build_verify
- run: dart run build_verify
Advanced Customizations
Here are some advanced techniques to make your custom code generation even more powerful:
1. Handle Multiple Annotations
You can support multiple annotations by creating separate generators or by handling different annotations within a single generator:
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'my_annotation.dart';
import 'another_annotation.dart';
class MyMultiGenerator extends Generator {
@override
Future generate(LibraryElement library, BuildStep buildStep) async {
final buffer = StringBuffer();
for (final element in library.topLevelElements) {
if (element is ClassElement) {
final myAnnotation = TypeChecker.fromRuntime(MyAnnotation)
.firstAnnotationOf(element);
if (myAnnotation != null) {
final className = element.name;
buffer.writeln('''
class $${className}Helper {
String getHelperText() => 'Hello from MyAnnotation for class $className';
}
''');
}
final anotherAnnotation = TypeChecker.fromRuntime(AnotherAnnotation)
.firstAnnotationOf(element);
if (anotherAnnotation != null) {
final className = element.name;
buffer.writeln('''
class $${className}AnotherHelper {
String getAnotherHelperText() => 'Hello from AnotherAnnotation for class $className';
}
''');
}
}
}
return buffer.toString();
}
}
Update the build.yaml file to include the new Generator.
targets:
$default:
sources:
- lib/**
builders:
my_code_gen_app:my_multi_generator:
options:
some_option: true
2. Pass Options to Generators
Pass custom options to your generator via the build.yaml file:
In build.yaml:
targets:
$default:
sources:
- lib/**
builders:
my_code_gen_app:my_generator:
options:
helper_prefix: CustomHelper
In my_generator.dart:
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'my_annotation.dart';
class MyGenerator extends GeneratorForAnnotation {
final String helperPrefix;
MyGenerator(this.helperPrefix);
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (element is ClassElement) {
final className = element.name;
return '''
class ${helperPrefix}$${className} {
String getHelperText() => 'Hello from ${helperPrefix}$${className} for class $className';
}
''';
} else {
throw UnsupportedError(
'MyAnnotation can only be applied to classes. Failed on $element');
}
}
}
Add a new file named builder.dart in the lib directory to handle the option parameters for the Generator Class:
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'my_generator.dart';
Builder myGenerator(BuilderOptions options) =>
PartBuilder([MyGenerator(options.config['helper_prefix'] as String)], '.g.dart',
header: '''
// GENERATED CODE - DO NOT MODIFY BY HAND
''');
3. Generate Complex Data Structures
Use the analyzer package to inspect the annotated classes and generate more complex data structures, such as JSON serializers:
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'my_annotation.dart';
class MyJsonGenerator extends GeneratorForAnnotation {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (element is ClassElement) {
final className = element.name;
final fields = element.fields.where((field) => !field.isStatic).toList();
final buffer = StringBuffer();
buffer.writeln('Map $${className}ToJson(${className} instance) => {');
for (final field in fields) {
buffer.writeln(' '${field.name}': instance.${field.name},');
}
buffer.writeln('};');
return buffer.toString();
} else {
throw UnsupportedError(
'MyAnnotation can only be applied to classes. Failed on $element');
}
}
}
Conclusion
Creating custom code generation scripts in Flutter allows you to automate repetitive tasks, ensure consistency, and improve the maintainability of your projects. By leveraging tools like build_runner, analyzer, and source_gen, you can create powerful generators that streamline your development workflow. Whether you’re generating simple helper classes or complex data structures, custom code generation is a valuable technique for any Flutter developer.