Flutter Macros: Wave Goodbye to Boilerplate

Welcome to the future of Flutter development! Imagine a world where the tedious task of writing boilerplate code is a thing of the past. Say hello to Dart Macros – your new secret weapon for supercharging productivity. These powerful runtime code generation tools work their magic behind the scenes, seamlessly reducing boilerplate and eliminating the need for cumbersome secondary tools. With Flutter macros, you can focus on crafting beautiful, high-performance apps while the macros handle the repetitive tasks, making your development process faster and more efficient than ever before. Get ready to unlock a new level of coding freedom and efficiency with Flutter macros!

So, what are macros?

A Dart macro is a customisable code snippet that takes other code as input and processes it in real-time to create, modify, or add new elements. Macros offer a reusable way to handle repetitive tasks and patterns, especially when dealing with the need to iterate over the fields of a class. Right now there’s one macro ready to go: JsonEncodable. As the name suggests, JsonEncodable generates the tedious fromJson and toJson methods for you.

Macros don’t require anything extra to run, just add the appropriate annotation that you need to your code and observe as the boilerplate code is generated in real time! With them, code is not written to the disk, so say goodbye to the part keyword. Macros directly augment the existing class.

What problem are they solving

Macros, as a static metaprogramming tool, provide an alternative to runtime code generation solutions (such as build_runner, json_serializable). Macros eliminate the need for a secondary tool, being integrated into Dart language, executing automatically in the background by Dart tools with every keystroke of your keyboard

  • No extra steps: Macros build your code in real-time as you write.
  • No performance hit: All the code generation happens directly in the compiler.
  • Clean and organised: No extra files cluttering up your project.
  • Clear error messages: Custom diagnostics appear right in your IDE, just like regular compiler messages.

Examples

Let’s compare 3 different code snippets, first without any code gen, second with Freezed and then third with macros

No Code Generation

class Facility {
  final int id;
  final String name;
  final String description;
  final Address address;
  final double lat;
  final double lon;

  Map<String, dynamic> toJson() {
       return {
         'id': id,
         'name': name,
         'description': description,
         'address': address.toJson(),
         'lat': lat,  
         'lon': lon,
  };

factory Facility.fromJson(Map<String, dynamic> json) {
      return Facility(
         id: json['id'],
         name: json['name'],
         description: json['description'],
         address: Address.fromJson(json['address']),
         lat: json['lat'],  
         lon: json['lon'],
			);
   }
}

Just typing this part out took a lot longer than it should for such a generic boilerplate. Obviously this is not even the whole class, it’s still missing the hashCode, == operator and possibly the toString overrides, and then do this for every (or at least most) model classes, and it turns out you’ve spent a lot of time on boilerplate code that is a lot of the time - error prone.

Now let’s look at Freezed

part 'facility.freezed.dart';
part 'facility.g.dart';

@Freezed()
class Facility with _$Facility {
      const Factory Facility({
        required int id,
        required String name,
        required String description,
        required Address address,
        required double lat,
        required double lon,	
      }) = _Facility;
      const Facility._();
      factory Facility.fromJson(Map<String,dynamic> json) => _$FacilityFromJson(json);
}

This looks a lot less error prone than manually writing the functions, but now you’ve got 2 new files that you need to work around, push to the project github or set up .gitignore. A lot less work than with manual writing, but still there are some parts of these files that could be considered as boilerplate.

Now let’s check it with macros

import 'package:json/json.dart';

@JsonEncodable()
class Facility {
  final int id;
  final String name;
  final String description;
  final Address address;
  final double lat;
  final double lon;
}

And that’s it! No other code needed to make this work. While you are typing the last semicolon the code is already generated and ready to be used. After writing this class we can go to some other file, and be able to do things like this:

void main() {
    var totallyValidFacilityJson = {...};

   // Use the generated members:
   var facility = Facility.fromJson(userJson);
   print(facility);
   print(facility.toJson());
}

Macros help you focus on the important stuff—the core logic of your product—and free you from the time-consuming, boring tasks that are essential but often get in the way.

It’s experimental - how to try it out

Set up the environment:

  1. Switch to the Dart dev channel or flutter master channel
  2. Run dart –version to make sure you have Dart version `3.5.0-152` or later
  3. Edit the SDK constraint in your pubspec to require Dart version: sdk: ^3.5.0-152.
  4. Add the package json to dependencies: dart pub add json.
  5. Enable the experiment in your package's analysis_options.yaml file. file at the root of your project:
analyzer:
 enable-experiment:
   - macros

  1. Import the package to the file that you’re planning to use import 'package:json/json.dart';
  1. Run the project with the experimental flag dart run --enable-experiment=macros bin/my_app.dart

Example of how to create custom macros

Your first custom macro should have two key segments, the macro keyword and an interface which determines for which interface the macro is determined. You can find the list of existing interfaces here.

For example, a macro that is applicable to enums, and adds new declarations to the enum, would implement the EnumDefinitionMacro interface:

macro class MyFirstMacro implements EnumDefinitionMacro {
   const MyFirstMacro();

   // ...
}

With your custom macro created, you are ready to add it to a declaration, like below:

@MyFirstMacro()
class MacroExperimenting {}

The Important thing to keep in mind is that at a high-level macros are using builders methods to combine properties of your declaration with identifiers on those properties, gathered through introspection of the codebase

Stable release timeline

The release of macros to stable is, as of the time of writing, unknown. A specific date has not yet been set due to the fact that macros work by deeply introspecting the codebase in which they’re applied. The general target for the release of JsonEncodable macro is projected to be until the end of 2024, and the stable release of the full functionality, including creating custom macros, for early-to-mid 2025.

FAQ

What is augmentation in dart?

Augmentation via the ‘augment’ keyword is the next step of evolution of the part keyword and part files, just without the extra syntax that makes your classes less clean. Augmentation allows defining class functions outside of the main class file.

For example we have this main class Person:

class Person {
    Person(this.name);

    final String name;
}

Now with the base class set, we can create our own json serialisation methods in a different part of the project, like this:

library augment 'models.dart';

augment class Person {
  Person.from(Map<String, dynamic> data) : name = data['name'];

  dynamic toJson() => {
    'name': name,
  };
}

And after adding the import below, you will be good to go!

import augment 'models_json.dart';

Why migrate from build_runner/freezed to macros? 

To improve the developer experience, of course! There is nothing wrong with these packages, they do the job perfectly, but with macros we make code cleaner by removing the _$ syntax needed for the packages mentioned above, as well as reduce the amount of workload on the developer to manage gitignored files or on peer review scroll endlessly over generated files to get to the crux of the pull request.

Our cheerful senior developer Nikola is based in Novi Sad, Serbia. There isn’t much he doesn’t know about Flutter and app development. He's a huge basketball fan and is often seen emulating his favourite team the Denver Nuggets on the basketball courts of Novi Sad

We'd love to show you how we can help

Get in Touch  

Latest Articles

All Articles