How is it possible that some code can be reduced from 70 lines of code to just 2?
With macros + primary constructors @dart_lang becomes insane π Example: from a 50+ lines of code to just 1 Stay tuned, because this is going to be game-changing π€―
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.
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.