2024 Flutter 重大更新,Dart 宏(Macro

说起宏编程可能大家并不陌生,但是这对于 Flutter 和 Dart 开发者来说它一直是一个「遗憾」,这个「遗憾」体现在编辑过程的代码修改支持上,其中最典型的莫过于 Dart 的 JSON 序列化。

举个例子,目前 Dart 语言的 JSON 序列化高度依赖 build_runner 去生成 Dart 代码,例如在实际使用中我们需要:

  • 依赖 json_serializable ,通过注解声明一个 Event 对象
  • 运行 flutter packages pub run build_runner build 生成文件
  • 得到 Event.g.dart 文件,在项目中使用它去实现 JSON 的序列化和反序列化

这里最大的问题在于,我们需要通过命令行去生成一个项目文件,并且这个文件我们还可以随意手动修改,从开发角度来说,这并不优雅也不方便。

而宏声明是用户定义的 Dart 类,它可以实现一个或多个新的内置宏接口,Dart 中的宏是用正常的命令式 Dart 代码来开发,不存在单独的“宏语言”

大多数宏并不是简单地从头开始生成新代码,而是根据程序的现有属性去添加代码,例如向 Class 添加 JSON 序列化的宏,可能会查看 Class 声明的字段,并从中合成一个 toJson() ,将这些字段序列化为 JSON 对象。

我们首先看一段官方的 Demo , 如下代码所示,可以看到 :

  • MyState 添加了一个自定义的 @AutoDispose() 注解,这是一个开发者自己实现的宏声明,并且继承了 State 对象,带有 dispose 方法。
  • MyState 里有多个 aa2bc 三个对象,其中 aa2b 都实现了 Disposable 接口,都有 dispose 方法
  • 虽然 aa2bMyStatedispose(); 方法来自不同基类实现,但是基于 @AutoDispose() 的实现,在代码调用 state.dispose(); 时, aa2b 变量的 dispose 方法也会被同步调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
dart复制代码import 'package:macro_proposal/auto_dispose.dart';

void main() {
 var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');
 state.dispose();
}

@AutoDispose()
class MyState extends State {
 final ADisposable a;
 final ADisposable? a2;
 final BDisposable b;
 final String c;

 MyState({required this.a, this.a2, required this.b, required this.c});

 @override
 String toString() => 'MyState!';
}

class State {
 void dispose() {
   print('disposing of $this');
}
}

class ADisposable implements Disposable {
 void dispose() {
   print('disposing of ADisposable');
}
}

class BDisposable implements Disposable {
 void dispose() {
   print('disposing of BDisposable');
}
}

如下图所示,可以看到,尽管 MyState 没用主动调用 aa2b 变量的 dispose 方法,并且它们和 MyStatedispose 也来自不同基类,但是最终执行所有 dispose 方法都被成功调用,这就是@AutoDispose() 的宏声明实现在编译时对代码进行了调整。

如下图所示是 @AutoDispose() 的宏编程实现,其中 macro 就是一个标志性的宏关键字,剩下的代码可以看到基本就是 dart 脚本的实现, macro 里主要是实现 ClassDeclarationsMacrobuildDeclarationsForClass方法,如下代码可以很直观看到关于 super.dispose();disposeCalls 的相关实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
typescript复制代码import 'package:_fe_analyzer_shared/src/macros/api.dart';

// Interface for disposable things.
abstract class Disposable {
 void dispose();
}

macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {
 const AutoDispose();

 @override
 void buildDeclarationsForClass(
     ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
   var methods = await builder.methodsOf(clazz);
   if (methods.any((d) => d.identifier.name == 'dispose')) {
     // Don't need to add the dispose method, it already exists.
     return;
  }

   builder.declareInType(DeclarationCode.fromParts([
     // TODO: Remove external once the CFE supports it.
     'external void dispose();',
  ]));
}

 @override
 Future<void> buildDefinitionForClass(
     ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
   var disposableIdentifier =
       // ignore: deprecated_member_use
       await builder.resolveIdentifier(
           Uri.parse('package:macro_proposal/auto_dispose.dart'),
           'Disposable');
   var disposableType = await builder
      .resolve(NamedTypeAnnotationCode(name: disposableIdentifier));

   var disposeCalls = <Code>[];
   var fields = await builder.fieldsOf(clazz);
   for (var field in fields) {
     var type = await builder.resolve(field.type.code);
     if (!await type.isSubtypeOf(disposableType)) continue;
     disposeCalls.add(RawCode.fromParts([
       '\n',
       field.identifier,
       if (field.type.isNullable) '?',
       '.dispose();',
    ]));
  }

   // Augment the dispose method by injecting all the new dispose calls after
   // either a call to `augmented()` or `super.dispose()`, depending on if
   // there already is an existing body to call.
   //
   // If there was an existing body, it is responsible for calling
   // `super.dispose()`.
   var disposeMethod = (await builder.methodsOf(clazz))
      .firstWhere((method) => method.identifier.name == 'dispose');
   var disposeBuilder = await builder.buildMethod(disposeMethod.identifier);
   disposeBuilder.augment(FunctionBodyCode.fromParts([
     '{\n',
     if (disposeMethod.hasExternal || !disposeMethod.hasBody)
       'super.dispose();'
     else
       'augmented();',
    ...disposeCalls,
     '}',
  ]));
}
}

到这里大家应该可以直观感受到宏编程的魅力,上述 Demo 来自 dart-languagemacros/example/auto_dispose_main ,其中 bin/ 目录下的代码是运行的脚本示例,lib/ 目录下的代码是宏编程实现的示例:

github.com/dart-lang/l…

当然,因为现在是实验性阶段,API 和稳定性还有待商榷,所以想运行这些 Demo 还需要一些额外的处理,比如版本强关联,例如上述的 auto_dispose_main 例子:

  • 需要 dart sdk 3.4.0-97.0.dev ,目前你可以通过 master 分支下载这个 dark-sdk storage.googleapis.com/dart-archiv…
  • 将 sdk 配置到环境变量,或者进入到 dart sdk 的 bin 目录执行 ./dart –version 检查版本
  • 进入上诉的 example 下执行 dart pub get,过程可能会有点长

  • 最后,执行 dart --enable-experiment=macros bin/auto_dispose_main.dart记得这个 dart 是你指定版本的 dart

另外,还有一个第三方例子是来自 millsteedmacros ,这是一个简单的 JSON 序列化实现 Demo ,并且可以直接不用额外下载 dark-sdk,通过某个 flutter 内置 dart-sdk 版本就可以满足条件:3.19.0-12.0.pre

在本地 Flutter 目录下,切换到 git checkout 3.19.0-12.0.pre ,然后执行 flutter doctor 初始化 dark sdk 即可。

代码的实现很简单,首先看 bin 下的示例,通过 @Model()GetUsersResponseUser 声明为 JSON 对象,然后在运行时,宏编程会自动添加 fromJsontoJson 方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
dart复制代码import 'dart:convert';

import 'package:macros/model.dart';

@Model()
class User {
 User({
   required this.username,
   required this.password,
});

 final String username;
 final String password;
}

@Model()
class GetUsersResponse {
 GetUsersResponse({
   required this.users,
   required this.pageNumber,
   required this.pageSize,
});

 final List<User> users;
 final int pageNumber;
 final int pageSize;
}

void main() {
 const body = '''
   {
     "users": [
       {
         "username": "ramon",
         "password": "12345678"
       }
     ],
     "pageNumber": 1,
     "pageSize": 30
   }
 ''';
 final json = jsonDecode(body) as Map<String, dynamic>;
 final response = GetUsersResponse.fromJson(json);
 final ramon = response.users.first;
 final millsteed = ramon.copyWith(username: 'millsteed', password: '87654321');
 final newResponse = response.copyWith(users: [...response.users, millsteed]);
 print(const JsonEncoder.withIndent(' ').convert(newResponse));
}

Model 的宏实现就相对复杂一些,但是实际上就是将类似 freezed/ json_serializable 是实现调整到宏实现了,而最终效果就是,开发者使用起来更加优雅了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
dart复制代码// ignore_for_file: depend_on_referenced_packages, implementation_imports

import 'dart:async';

import 'package:_fe_analyzer_shared/src/macros/api.dart';

macro class Model implements ClassDeclarationsMacro {
 const Model();

 static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];
 static const _collectionTypes = ['List'];

 @override
 Future<void> buildDeclarationsForClass(
   ClassDeclaration classDeclaration,
   MemberDeclarationBuilder builder,
) async {
   final className = classDeclaration.identifier.name;

   final fields = await builder.fieldsOf(classDeclaration);

   final fieldNames = <String>[];
   final fieldTypes = <String, String>{};
   final fieldGenerics = <String, List<String>>{};

   for (final field in fields) {
     final fieldName = field.identifier.name;
     fieldNames.add(fieldName);

     final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
     fieldTypes[fieldName] = fieldType;

     if (_collectionTypes.contains(fieldType)) {
       final generics = (field.type.code as NamedTypeAnnotationCode)
          .typeArguments
          .map((e) => (e as NamedTypeAnnotationCode).name.name)
          .toList();
       fieldGenerics[fieldName] = generics;
    }
  }

   final fieldTypesWithGenerics = fieldTypes.map(
    (name, type) {
       final generics = fieldGenerics[name];
       return MapEntry(
         name,
         generics == null ? type : '$type<${generics.join(', ')}>',
      );
    },
  );

   _buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);
   _buildToJson(builder, fieldNames, fieldTypes);
   _buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);
   _buildToString(builder, className, fieldNames);
   _buildEquals(builder, className, fieldNames);
   _buildHashCode(builder, fieldNames);
}

 void _buildFromJson(
   MemberDeclarationBuilder builder,
   String className,
   List<String> fieldNames,
   Map<String, String> fieldTypes,
   Map<String, List<String>> fieldGenerics,
) {
   final code = [
     'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
     'return $className('.indent(4),
     for (final fieldName in fieldNames) ...[
       if (_baseTypes.contains(fieldTypes[fieldName])) ...[
         "$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
            .indent(6),
      ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
         "$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
         '.whereType<Map<String, dynamic>>()'.indent(10),
         '.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
         '.toList(),'.indent(10),
      ] else ...[
         '$fieldName: ${fieldTypes[fieldName]}'
                 ".fromJson(json['$fieldName'] "
                 'as Map<String, dynamic>),'
            .indent(6),
      ],
    ],
     ');'.indent(4),
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}

 void _buildToJson(
   MemberDeclarationBuilder builder,
   List<String> fieldNames,
   Map<String, String> fieldTypes,
) {
   final code = [
     'Map<String, dynamic> toJson() {'.indent(2),
     'return {'.indent(4),
     for (final fieldName in fieldNames) ...[
       if (_baseTypes.contains(fieldTypes[fieldName])) ...[
         "'$fieldName': $fieldName,".indent(6),
      ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
         "'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),
      ] else ...[
         "'$fieldName': $fieldName.toJson(),".indent(6),
      ],
    ],
     '};'.indent(4),
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}

 void _buildCopyWith(
   MemberDeclarationBuilder builder,
   String className,
   List<String> fieldNames,
   Map<String, String> fieldTypes,
) {
   final code = [
     '$className copyWith({'.indent(2),
     for (final fieldName in fieldNames) ...[
       '${fieldTypes[fieldName]}? $fieldName,'.indent(4),
    ],
     '}) {'.indent(2),
     'return $className('.indent(4),
     for (final fieldName in fieldNames) ...[
       '$fieldName: $fieldName ?? this.$fieldName,'.indent(6),
    ],
     ');'.indent(4),
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}

 void _buildToString(
   MemberDeclarationBuilder builder,
   String className,
   List<String> fieldNames,
) {
   final code = [
     '@override'.indent(2),
     'String toString() {'.indent(2),
     "return '$className('".indent(4),
     for (final fieldName in fieldNames) ...[
       if (fieldName != fieldNames.last) ...[
         "'$fieldName: $$fieldName, '".indent(8),
      ] else ...[
         "'$fieldName: $$fieldName'".indent(8),
      ],
    ],
     "')';".indent(8),
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}

 void _buildEquals(
   MemberDeclarationBuilder builder,
   String className,
   List<String> fieldNames,
) {
   final code = [
     '@override'.indent(2),
     'bool operator ==(Object other) {'.indent(2),
     'return other is $className &&'.indent(4),
     'runtimeType == other.runtimeType &&'.indent(8),
     for (final fieldName in fieldNames) ...[
       if (fieldName != fieldNames.last) ...[
         '$fieldName == other.$fieldName &&'.indent(8),
      ] else ...[
         '$fieldName == other.$fieldName;'.indent(8),
      ],
    ],
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}

 void _buildHashCode(
   MemberDeclarationBuilder builder,
   List<String> fieldNames,
) {
   final code = [
     '@override'.indent(2),
     'int get hashCode {'.indent(2),
     'return Object.hash('.indent(4),
     'runtimeType,'.indent(6),
     for (final fieldName in fieldNames) ...[
       '$fieldName,'.indent(6),
    ],
     ');'.indent(4),
     '}'.indent(2),
  ].join('\n');
   builder.declareInType(DeclarationCode.fromString(code));
}
}

extension on String {
 String indent(int length) {
   final space = StringBuffer();
   for (var i = 0; i < length; i++) {
     space.write(' ');
  }
   return '$space$this';
}
}

目前宏还处于试验性质的阶段,所以 API 还在调整,这也是为什么上面的例子需要指定 dart 版本的原因,另外宏目前规划里还有一些要求,例如

  • 所有宏构造函数都必须标记为 const
  • 所有宏必须至少实现其中一个 Macro 接口
  • 宏不能是抽象对象
  • 宏 class 不能由其他宏生成
  • 宏 class 不能包含泛型类型参数
  • 每个宏接口都需要声明宏类必须实现的方法,例如,在声明阶段应用的 ClassDeclarationsMacro及其buildDeclarationsForClass方法。

未来规划里,宏 API 可能会作为 Pub 包提供,通过库 dart:_macros来提供支持 ,具体还要等正式发布时 dart 团队的决策。

总的来说,这对于 dart 和 flutter 是一个重大的厉害消息,虽然宏编程并不是什么新鲜概念,该是 dart 终于可以优雅地实现 JSON 序列化,并且还是用 dart 来实现,这对于 flutter 开发者来说,无疑是最好的新年礼物。

所以,新年快乐~我们节后再见~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%