Photo by Aaron Burden on Unsplash

Introduction

Reducing code repetition and increasing efficiency are important objectives in the field of software development. Google’s Dart programming language has undergone substantial development to offer tools and features that facilitate the accomplishment of these objectives. Dart 3.4.0 brought macros, one of the most potent and recent improvements to the Dart ecosystem.

When it comes to handling repetitive and common activities in development, macros provide a more integrated and efficient solution by enabling automatic code generation at compile time.

Specifically, macros can be a perfect substitute for well-known libraries like freezed and json_serializable, which are typically used for creating immutable classes and serializing JSON, respectively.

Benefits of using macros in Dart

Direct Language Integration. Since macros are directly incorporated into the language, they offer more consistency and compatibility with native features of the language than external libraries do.Reducing External Dependencies. You can lessen your application’s size and simplify dependency management by utilizing macros to cut down on your reliance on external libraries.Compile-Time Code Generation. Compared to solutions that generate code at runtime, macros that generate code at compile time may provide faster results.Increased Customization and freedom. Developers can better tailor code generation to the unique needs of their projects with the help of macros, which provide a great deal of customization freedom.

Comparing freezed and json_serializable

json_serializable

In order to generate JSON serialization code in Dart, this library has been commonly utilized. Still, it needs more dependencies and setups. Without the need for additional tools, macros can generate fromJson and toJson methods automatically right within the source code.

freezed

Using this library, creating type-safe, immutable Dart classes is a breeze. While freezed is incredibly strong, macros may accomplish comparable tasks with less dependencies and more direct integration.

Configuring the environment

You must set up your development environment correctly in order to utilize Dart’s macro features. Here’s a step-by-step approach to installing the required dependencies and modifying the pubspec.yaml file in your Dart/Flutter project so you may use macros.

Step 1: Start a New Flutter/Dart Project

You can use the Dart or Flutter commands to start a new project if you don’t already have one.

Dart project:

dart create my_macro_project
cd my_macro_project

Flutter project:

flutter create my_macro_project
cd my_macro_project

Step 2: Revise the file pubspec.yaml

The next step is to update the pubspec.yaml file to include the necessary dependencies. Macros in Dart are found in specific packages that you must add to your project.

Open the pubspec.yaml file and add the following dependencies:

dependencies:
dart_sdk: ‘>=3.4.0 <4.0.0’

dev_dependencies:
build_runner: ^2.1.7
macro_builder: ^0.1.0
analyzer: ^5.8.0

The pubspec.yaml file should look similar to this:

name: my_macro_project
description: A new Dart/Flutter project with macros
version: 1.0.0+1

environment:
sdk: ‘>=3.4.0 <4.0.0’

dependencies:
flutter:
sdk: flutter

dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.1.7
macro_builder: ^0.1.0
analyzer: ^5.8.0

flutter:
uses-material-design: true

Step 3: Install the dependencies

After updating the pubspec.yaml file, use the terminal to execute the following command to install the dependencies:

flutter pub get

If you are working on a pure Dart project:

dart pub get

Step 4: Macro configuration

To use macros in your project, you must generate and configure particular macro files. Create a folder called macros at the root of your project and a file called json_macro.dart within it.

This is the content of the json_macro.dart file:

import ‘package:macro_builder/macro_builder.dart’;

class JsonSerializableMacro implements ClassMacro {
const JsonSerializableMacro();

@override
void visitClassDeclaration(ClassDeclaration declaration, ClassDefinitionBuilder builder) {
builder.addToClass(Method(
name: ‘fromJson’,
returnType: ‘dynamic’,
positionalParameters: [Parameter(‘json’, type: ‘Map<String, dynamic>’)],
body: _generateFromJson(declaration),
));
builder.addToClass(Method(
name: ‘toJson’,
returnType: ‘Map<String, dynamic>’,
body: _generateToJson(declaration),
));
}

String _generateFromJson(ClassDeclaration declaration) {
final fields = declaration.fields.map((field) {
return “${field.name}: json[‘${field.name}’],”;
}).join();
return ‘return ${declaration.name}($fields);’;
}

String _generateToJson(ClassDeclaration declaration) {
final fields = declaration.fields.map((field) {
return “‘${field.name}’: ${field.name},”;
}).join();
return ‘return {$fields};’;
}
}

Step 5: Run the builder

To generate the code from your macros, you need to run the builder. Use the following command to start the process on Dart project:

dart run build_runner build

Or for a Flutter project:

flutter pub run build_runner build

This command will scan your code and apply the macros where they are defined, generating the necessary code automatically.

Example 1. Replace json_serializable with macros

In this part, we will demonstrate how to use Dart macros to replace the json_serializable feature. We’ll utilize the JsonSerializableMacro macro, which generates the fromJson and toJson methods for a model class. Below is a complete example of how to apply this macro to a class and generate the required code.

Applying the macro to a model class

We’ve already defined the JsonSerializableMacro macro in the json_macro.dart file. Now let’s add this macro to the User model class.

import ‘macros/json_macro.dart’;

@JsonSerializableMacro()
class User {
final String name;
final int age;

User({required this.name, required this.age});
}

Code generation

To generate code using macros, we need to ensure that our build.yaml file is configured correctly to process files containing macros.

build.yaml

targets:
$default:
builders:
macro_builder:
generate_for:
– lib/**.dart

We execute the following command in the terminal to generate the code on Dart project:

dart run build_runner build

In Flutter projects:

flutter pub run build_runner build

This command will process the user.dart file, apply the JsonSerializableMacro macro, and automatically generate the fromJson and toJson methods.

Generated code

After running the builder, the following code will be generated automatically:

user.g.dart

part of ‘user.dart’;

extension UserJson on User {
static User fromJson(Map<String, dynamic> json) {
return User(
name: json[‘name’] as String,
age: json[‘age’] as int,
);
}

Map<String, dynamic> toJson() {
return {
‘name’: name,
‘age’: age,
};
}
}

Example 2. Replacing frozen with macros

In this section, we will look at how to write a macro that builds immutable classes using copy (copyWith), equality (==), and hashCode methods. We will walk you through the process of applying this macro to a class and generating the required code.

Define the macro for immutable classes

First, we create a macro that generates the copyWith, ==, and hashCode methods for any class annotated with this macro. We’ll make a file called immutable_macro.dart in the macros directory.

import ‘package:macro_builder/macro_builder.dart’;

class ImmutableMacro implements ClassMacro {
const ImmutableMacro();

@override
void visitClassDeclaration(ClassDeclaration declaration, ClassDefinitionBuilder builder) {
builder.addToClass(Method(
name: ‘copyWith’,
returnType: declaration.name,
parameters: declaration.fields.map((field) => Parameter(field.name, type: field.type, isOptional: true)).toList(),
body: _generateCopyWith(declaration),
));
builder.addToClass(Method(
name: ‘==’,
returnType: ‘bool’,
parameters: [Parameter(‘other’, type: ‘Object’)],
body: _generateEquals(declaration),
));
builder.addToClass(Method(
name: ‘hashCode’,
returnType: ‘int’,
body: _generateHashCode(declaration),
));
}

String _generateCopyWith(ClassDeclaration declaration) {
final fields = declaration.fields.map((field) {
final name = field.name;
return “$name: $name ?? this.$name,”;
}).join();
return ‘return ${declaration.name}($fields);’;
}

String _generateEquals(ClassDeclaration declaration) {
final fields = declaration.fields.map((field) {
final name = field.name;
return “this.$name == other.$name”;
}).join(‘ && ‘);
return ”’
if (identical(this, other)) return true;
if (other is! ${declaration.name}) return false;
return $fields;
”’;
}

String _generateHashCode(ClassDeclaration declaration) {
final fields = declaration.fields.map((field) {
final name = field.name;
return “$name.hashCode”;
}).join(‘ ^ ‘);
return ‘return $fields;’;
}
}

Applying the macro to a model class

Now that we have defined the ImmutableMacro macro, let’s apply it to a model class called User.

import ‘macros/immutable_macro.dart’;

@ImmutableMacro()
class User {
final String name;
final int age;

User({required this.name, required this.age});
}

Code generation

To generate code using macros, we ensure that the build.yaml file is configured correctly to process files that contain macros.

build.yaml

targets:
$default:
builders:
macro_builder:
generate_for:
– lib/**.dart

We execute the following command in the terminal to generate the code on Dart project:

dart run build_runner build

In Flutter projects:

flutter pub run build_runner build

This command will process the user.dart file, apply the ImmutableMacro macro, and automatically generate the copyWith, ==, and hashCode methods.

Generated code

After running the builder, the following code will be generated automatically:

user.g.dart

part of ‘user.dart’;

extension UserImmutable on User {
User copyWith({String? name, int? age}) {
return User(
name: name ?? this.name,
age: age ?? this.age,
);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! User) return false;
return name == other.name && age == other.age;
}

@override
int get hashCode => name.hashCode ^ age.hashCode;
}

Practical case. Receiving JSON from a server

Now, we will show how to receive a JSON from a server and transform it into a data model using macros in Dart. This example will include configuring the server, receiving data, and applying macros to transform that data.

Step 1: Server configuration

To simulate receiving a JSON from a server, we will configure a simple server using http package in Dart. This server will send JSON data which we will then transform into a data model.

Simulated server call (server.dart)

import ‘dart:convert’;
import ‘dart:io’;

void main() {
var server = HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
server.then((HttpServer server) {
print(“Server is running on localhost:8080”);
server.listen((HttpRequest request) {
if (request.method == ‘GET’ && request.uri.path == ‘/user’) {
var jsonResponse = jsonEncode({
‘name’: ‘John Doe’,
‘age’: 30,
});
request.response
..statusCode = HttpStatus.ok
..headers.contentType = ContentType.json
..write(jsonResponse)
..close();
}
});
});
}

Step 2. Receiving data from the server

We will use the http package in Dart to make a GET request to the server and receive the JSON data.

HTTP request and data transformation (main.dart)

import ‘dart:convert’;
import ‘package:http/http.dart’ as http;
import ‘user.dart’;

Future<Map<String, dynamic>> fetchUserData() async {
final response = await http.get(Uri.parse(‘http://localhost:8080/user’));

if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(‘Failed to load user data’);
}
}

Future<void> main() async {
final userData = await fetchUserData();
final user = User.fromJson(userData);

print(‘User: ${user.name}, Age: ${user.age}’);
}

Conclusion

The addition of macros to Google’s Dart programming language, particularly in version 3.4.0, represents a significant step toward reducing code redundancy and increasing productivity in software development.

Macros, which are completely integrated into Dart, provide a unified alternative to external libraries, promoting consistency and compatibility throughout the language ecosystem. This integration not only streamlines development processes, but also minimizes the need for additional dependencies, resulting in cleaner and more manageable codebases.

Furthermore, macros’ ability to execute code generation during compile time improves efficiency over runtime alternatives while also giving developers with greater customization and flexibility. Practical examples highlight macros’ ability to automate activities such as JSON serialization and immutable class generation, allowing developers to focus on essential functions rather than repetitive writing.

Simplifying Software Development with Dart Macros was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.