Generating Code to Reduce Boilerplate in Flutter

Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, is known for its rapid development capabilities. However, like any development framework, Flutter can sometimes suffer from boilerplate code – repetitive sections of code that are necessary but add little value. Fortunately, Flutter offers several mechanisms to reduce this boilerplate, making your code cleaner, more maintainable, and easier to read. Code generation is one of the most powerful techniques for automating repetitive coding tasks, thus reducing boilerplate. This article delves into using code generation to minimize boilerplate in Flutter applications.

What is Boilerplate Code?

Boilerplate code refers to sections of code that are repeated multiple times with little to no variation. Examples in Flutter include:

  • Creating models from JSON data (serialization and deserialization)
  • Implementing copyWith methods
  • Generating routing configurations
  • Handling different states in BLoC or Riverpod

Why Reduce Boilerplate?

  • Improved Readability: Less repetitive code makes the core logic clearer.
  • Increased Maintainability: Changes are easier to implement when code is concise and well-structured.
  • Reduced Errors: Automated code generation reduces the chance of human error.
  • Faster Development: Automating repetitive tasks saves significant development time.

Methods to Reduce Boilerplate in Flutter

Here are several techniques to minimize boilerplate code in Flutter:

  1. Using Code Generation: Employ build_runner and code-generation libraries.
  2. Creating Reusable Widgets: Develop custom widgets for common UI patterns.
  3. Implementing Helper Functions: Use utility functions to handle repetitive tasks.
  4. Leveraging Functional Programming: Apply functional programming techniques for concise code.
  5. Adopting Design Patterns: Employ proven architectural patterns like BLoC or Riverpod.

Generating Code with build_runner

The build_runner package is a powerful tool for code generation in Flutter. It works in conjunction with code-generation libraries to automate the creation of repetitive code based on annotations in your code.

Step 1: Add Dependencies

Add the following dependencies to your pubspec.yaml file:

dependencies:
  json_annotation: ^4.8.1 # or newer
dev_dependencies:
  build_runner: ^2.4.6 # or newer
  json_serializable: ^6.9.0 # or newer

Here’s a brief explanation:

  • json_annotation: Contains annotations for marking classes and fields for JSON serialization.
  • build_runner: The build system to generate code.
  • json_serializable: Generates the code for JSON serialization and deserialization.

Step 2: Create a Model Class

Annotate your model class with @JsonSerializable():

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int userId;
  final String username;
  final String email;

  User({required this.userId, required this.username, required this.email});

  factory User.fromJson(Map json) => _$UserFromJson(json);

  Map toJson() => _$UserToJson(this);
}

Explanation:

  • part 'user.g.dart';: This line tells the generator where to output the generated code.
  • @JsonSerializable(): Annotates the class for JSON serialization.
  • The fromJson and toJson methods are generated automatically by the json_serializable package.

Step 3: Run the Code Generator

Execute the following command in your terminal:

flutter pub run build_runner build

Or, for continuous generation during development:

flutter pub run build_runner watch

This command generates the user.g.dart file, which includes the _$UserFromJson and _$UserToJson functions for handling JSON conversion.

Step 4: Use the Generated Code

You can now easily serialize and deserialize your model:

import 'user.dart';
import 'dart:convert';

void main() {
  final user = User(userId: 1, username: 'john_doe', email: 'john.doe@example.com');
  final json = user.toJson();

  print('JSON: ${jsonEncode(json)}');

  final decodedUser = User.fromJson(json);
  print('User: ${decodedUser.username}');
}

Using Other Code Generation Libraries

Besides JSON serialization, you can use code generation for other tasks such as generating routing configurations or implementing state management.

AutoRoute for Route Generation

The AutoRoute package simplifies navigation and route management in Flutter. It generates navigation code based on your route definitions.

Step 1: Add Dependencies

dependencies:
  auto_route: ^7.0.0 # or newer
dev_dependencies:
  auto_route_generator: ^7.0.0 # or newer
  build_runner: ^2.4.6 # or newer

Step 2: Define Routes

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';

import 'home_page.dart';
import 'settings_page.dart';

part 'app_router.g.dart';

@AutoRouterConfig()
class AppRouter extends _$AppRouter {
  @override
  List get routes => [
        AutoRoute(page: HomePageRoute.page, initial: true),
        AutoRoute(page: SettingsPageRoute.page),
      ];
}

@RoutePage()
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: const Center(child: Text('Home Page')),
    );
  }
}

@RoutePage()
class SettingsPage extends StatelessWidget {
  const SettingsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: const Center(child: Text('Settings Page')),
    );
  }
}

Step 3: Run Code Generation

flutter pub run build_runner build

Step 4: Use the Generated Router

import 'package:flutter/material.dart';
import 'app_router.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final _appRouter = AppRouter();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: _appRouter.delegate(),
      routeInformationParser: _appRouter.defaultRouteParser(),
    );
  }
}

Freezed for Immutable Classes

Freezed is a code-generation package for creating immutable classes with features like copyWith, toString, and equality operators without writing boilerplate code.

Step 1: Add Dependencies

dependencies:
  freezed_annotation: ^2.4.1 # or newer
  flutter:
    sdk: flutter

dev_dependencies:
  freezed: ^2.4.6 # or newer
  build_runner: ^2.4.6 # or newer

Step 2: Define Your Class

import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';

@freezed
class Person with _$Person {
  const factory Person({
    required String name,
    required int age,
    String? address,
  }) = _Person;
}

Step 3: Run Code Generation

flutter pub run build_runner build

Step 4: Use the Generated Class

void main() {
  const person1 = Person(name: 'Alice', age: 30, address: '123 Main St');
  final person2 = person1.copyWith(age: 31);

  print('Person 1: ${person1.toString()}');
  print('Person 2: ${person2.toString()}');
}

Best Practices for Code Generation

  • Keep Generated Files Separate: Always output generated files to a separate directory to keep your source code clean.
  • Use Annotations Effectively: Use annotations to clearly define the intent and structure for code generation.
  • Automate the Build Process: Integrate the code generation process into your build pipeline to ensure consistency.
  • Version Control: Include generated files in your version control system so that everyone on your team can benefit from them.
  • Understand the Generated Code: While code generation automates tasks, understanding the generated code is important for debugging and customization.

Conclusion

Generating code to reduce boilerplate can significantly improve the development experience in Flutter. By using tools like build_runner, json_serializable, auto_route, and freezed, developers can automate repetitive coding tasks, leading to cleaner, more maintainable, and more efficient code. Adopting these practices not only speeds up development but also reduces the likelihood of errors, ensuring that your Flutter projects are of the highest quality.