β€’

tech

Macros, Static Metaprogramming, and Primary Constructors in Dart and Flutter

Some new features are coming to the dart programming language: static metaprogramming and primary constructors. Learn how macros, static metaprogramming, and primary constructors work, why they matter, and how to use them.


Sandro Maglione

Sandro Maglione

Software development

How is it possible that some code can be reduced from 70 lines of code to just 2?

Two upcoming language features will completely change how you write dart and flutter code:

Let's see what is the problem, why it matters, how it works, and how your dart code will change (forever πŸ”₯) πŸ‘‡


The problem with dart code

How is it possible that some code can be reduced from 70 lines of code to just 2?

Today to write a data class with equality (==), serialization/deserialization (toJson, fromJson), immutability (@immutable, copyWith) you need the following in dart:

@immutable
class Person {
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  factory Person.fromJson(Map<String, Object?> json) {
    return Person(
      firstName: json['firstName'] as String,
      lastName: json['lastName'] as String,
      age: json['age'] as int,
    );
  }

  final String firstName;
  final String lastName;
  final int age;

  Person copyWith({
    String? firstName,
    String? lastName,
    int? age,
  }) {
    return Person(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
      age: age ?? this.age,
    );
  }

  Map<String, Object?> toJson() {
    return {
      'firstName': firstName,
      'lastName': lastName,
      'age': age,
    };
  }

  @override
  String toString() {
    return 'Person('
        'firstName: $firstName, '
        'lastName: $lastName, '
        'age: $age'
        ')';
  }

  @override
  bool operator ==(Object other) {
    return other is Person &&
        other.runtimeType == runtimeType &&
        other.firstName == firstName &&
        other.lastName == lastName &&
        other.age == age;
  }

  @override
  int get hashCode {
    return Object.hash(
      runtimeType,
      firstName,
      lastName,
      age,
    );
  }
}

Notice πŸ‘†: These are 70 lines for a single class

Two new features are coming to dart that will cause a revolution:

Why revolution? No exaggeration here!

How so? Well, the above 70 lines will be shortened to just 2:

@Model() 
class Person(String firstName, String lastName, int age);

Macros and Primary Constructors in dart

Take a look at the highlighted snippet below, see if you notice the problem (and the solution):

@immutable
class Person {
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  factory Person.fromJson(Map<String, Object?> json) {
    return Person(
      firstName: json['firstName'] as String,
      lastName: json['lastName'] as String,
      age: json['age'] as int,
    );
  }

  final String firstName;
  final String lastName;
  final int age;

  Person copyWith({
    String? firstName,
    String? lastName,
    int? age,
  }) {
    return Person(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
      age: age ?? this.age,
    );
  }

  Map<String, Object?> toJson() {
    return {
      'firstName': firstName,
      'lastName': lastName,
      'age': age,
    };
  }

  @override
  String toString() {
    return 'Person('
        'firstName: $firstName, '
        'lastName: $lastName, '
        'age: $age'
        ')';
  }

  @override
  bool operator ==(Object other) {
    return other is Person &&
        other.runtimeType == runtimeType &&
        other.firstName == firstName &&
        other.lastName == lastName &&
        other.age == age;
  }

  @override
  int get hashCode {
    return Object.hash(
      runtimeType,
      firstName,
      lastName,
      age,
    );
  }
}

See how many times we need to repeat firstName, lastName, and age?

This is what in programming is called Boilerplate Code πŸ‘€

If you take a second look now it's easy to notice: the only "relevant" information in the class are the parameters and their types, everything else is repeated 🀯:

@immutable
class Person { 
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  factory Person.fromJson(Map<String, Object?> json) {
    return Person(
      firstName: json['firstName'] as String,
      lastName: json['lastName'] as String,
      age: json['age'] as int,
    );
  }

  final String firstName; 
  final String lastName; 
  final int age; 

  Person copyWith({
    String? firstName,
    String? lastName,
    int? age,
  }) {
    return Person(
      firstName: firstName ?? this.firstName,
      lastName: lastName ?? this.lastName,
      age: age ?? this.age,
    );
  }

  Map<String, Object?> toJson() {
    return {
      'firstName': firstName,
      'lastName': lastName,
      'age': age,
    };
  }

  @override
  String toString() {
    return 'Person('
        'firstName: $firstName, '
        'lastName: $lastName, '
        'age: $age'
        ')';
  }

  @override
  bool operator ==(Object other) {
    return other is Person &&
        other.runtimeType == runtimeType &&
        other.firstName == firstName &&
        other.lastName == lastName &&
        other.age == age;
  }

  @override
  int get hashCode {
    return Object.hash(
      runtimeType,
      firstName,
      lastName,
      age,
    );
  }
} 

Ideally we can reduce the code to just the following:

class Person {
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  final String firstName;
  final String lastName;
  final int age;
}

Guess what, we are not even satisfied with that πŸ™…β€β™‚οΈ

What matter at the end are the parameters:

class Person { 
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  final String firstName; 
  final String lastName; 
  final int age; 
} 

Primary constructors come to the rescue to solve this issue as well:

class Person (String firstName, String lastName, int age);

Finally, we need to actually apply the macro to this code, so we end up with the initial 2 lines:

@Model() 
class Person(String firstName, String lastName, int age);

There is more 🀩

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

How macros (will) work in dart

macro allows to write code that takes code as parameters.

Metaprogramming refers to code that can operate on other code as if it were data: take code in as parameters, reflect over it, inspect it, create it, modify it, and return it.

Static metaprogramming means doing that work at compile-time.

From official Dart's language specification

In practice in the above example this means having access in code to the class parameters:

/// This is not the real syntax, but just to give you an idea πŸ‘‡πŸ’‘ 
Code makeMacro(List<MacroParameter> parameters) {
  return /// Code to generate...
}

Having access to the parameters allows to generate all the boilerplate at compile-time:

/// This is not the real syntax, but just to give you an idea πŸ‘‡πŸ’‘ 
Code makeFactoryConstructor(List<MacroParameter> parameters) {
  String code = 'factory Person.fromJson(Map<String, Object?> json) {'
    '  return Person(';

  for (var i = 0; i < parameters.length; ++i) {
    final parameter = parameters[i];
    code += "    ${parameter.name}: json['${parameter.name}' as ${parameter.type}]";
  }

  code += '  );',
  '}';

  return code;
}

With these you can write code that writes other code (meta-programming indeed πŸ”„)

Resources

Both Static metaprogramming and Primary constructors are still "Being spec'ed".

⚠️ This means that the dart team is still exploring the API, how it will work, and what features will be included

You can subscribe to their respective Github issues to stay up to date with the discussion:

The feature specifications are always updated with the latest changes, you can follow them here below:


These 2 new features together have the potential to skyrocket dart to another dimension πŸš€

I plan to explore the progress and experiment with alpha and beta releases in the upcoming months. I will share more content and snippets as new details become available πŸ”œ

If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below πŸ‘‡

Thanks for reading.

πŸ‘‹γƒ»Interested in learning more, every week?

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.