2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救

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

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

  • 依赖 json_serializable ,通过注解声明一个 Event 对象
  • 运行 flutter packages pub run build_runner build 生成文件
  • 得到 Event.g.dart 文件,在项目中使用它去实现 JSON 的序列化和反序列化
2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救 - 图1 2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救 - 图2

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

而宏声明是用户定义的 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. import 'package:macro_proposal/auto_dispose.dart';
  2. void main() {
  3. var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');
  4. state.dispose();
  5. }
  6. @AutoDispose()
  7. class MyState extends State {
  8. final ADisposable a;
  9. final ADisposable? a2;
  10. final BDisposable b;
  11. final String c;
  12. MyState({required this.a, this.a2, required this.b, required this.c});
  13. @override
  14. String toString() => 'MyState!';
  15. }
  16. class State {
  17. void dispose() {
  18. print('disposing of $this');
  19. }
  20. }
  21. class ADisposable implements Disposable {
  22. void dispose() {
  23. print('disposing of ADisposable');
  24. }
  25. }
  26. class BDisposable implements Disposable {
  27. void dispose() {
  28. print('disposing of BDisposable');
  29. }
  30. }

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

2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救 - 图3

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

  1. import 'package:_fe_analyzer_shared/src/macros/api.dart';
  2. // Interface for disposable things.
  3. abstract class Disposable {
  4. void dispose();
  5. }
  6. macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {
  7. const AutoDispose();
  8. @override
  9. void buildDeclarationsForClass(
  10. ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
  11. var methods = await builder.methodsOf(clazz);
  12. if (methods.any((d) => d.identifier.name == 'dispose')) {
  13. // Don't need to add the dispose method, it already exists.
  14. return;
  15. }
  16. builder.declareInType(DeclarationCode.fromParts([
  17. // TODO: Remove external once the CFE supports it.
  18. 'external void dispose();',
  19. ]));
  20. }
  21. @override
  22. Future<void> buildDefinitionForClass(
  23. ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
  24. var disposableIdentifier =
  25. // ignore: deprecated_member_use
  26. await builder.resolveIdentifier(
  27. Uri.parse('package:macro_proposal/auto_dispose.dart'),
  28. 'Disposable');
  29. var disposableType = await builder
  30. .resolve(NamedTypeAnnotationCode(name: disposableIdentifier));
  31. var disposeCalls = <Code>[];
  32. var fields = await builder.fieldsOf(clazz);
  33. for (var field in fields) {
  34. var type = await builder.resolve(field.type.code);
  35. if (!await type.isSubtypeOf(disposableType)) continue;
  36. disposeCalls.add(RawCode.fromParts([
  37. '\n',
  38. field.identifier,
  39. if (field.type.isNullable) '?',
  40. '.dispose();',
  41. ]));
  42. }
  43. // Augment the dispose method by injecting all the new dispose calls after
  44. // either a call to `augmented()` or `super.dispose()`, depending on if
  45. // there already is an existing body to call.
  46. //
  47. // If there was an existing body, it is responsible for calling
  48. // `super.dispose()`.
  49. var disposeMethod = (await builder.methodsOf(clazz))
  50. .firstWhere((method) => method.identifier.name == 'dispose');
  51. var disposeBuilder = await builder.buildMethod(disposeMethod.identifier);
  52. disposeBuilder.augment(FunctionBodyCode.fromParts([
  53. '{\n',
  54. if (disposeMethod.hasExternal || !disposeMethod.hasBody)
  55. 'super.dispose();'
  56. else
  57. 'augmented();',
  58. ...disposeCalls,
  59. '}',
  60. ]));
  61. }
  62. }

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

https://github.com/dart-lang/language/tree/main/working/macros/example

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

另外,还有一个第三方例子是来自 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. import 'dart:convert';
  2. import 'package:macros/model.dart';
  3. @Model()
  4. class User {
  5. User({
  6. required this.username,
  7. required this.password,
  8. });
  9. final String username;
  10. final String password;
  11. }
  12. @Model()
  13. class GetUsersResponse {
  14. GetUsersResponse({
  15. required this.users,
  16. required this.pageNumber,
  17. required this.pageSize,
  18. });
  19. final List<User> users;
  20. final int pageNumber;
  21. final int pageSize;
  22. }
  23. void main() {
  24. const body = '''
  25. {
  26. "users": [
  27. {
  28. "username": "ramon",
  29. "password": "12345678"
  30. }
  31. ],
  32. "pageNumber": 1,
  33. "pageSize": 30
  34. }
  35. ''';
  36. final json = jsonDecode(body) as Map<String, dynamic>;
  37. final response = GetUsersResponse.fromJson(json);
  38. final ramon = response.users.first;
  39. final millsteed = ramon.copyWith(username: 'millsteed', password: '87654321');
  40. final newResponse = response.copyWith(users: [...response.users, millsteed]);
  41. print(const JsonEncoder.withIndent(' ').convert(newResponse));
  42. }

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

  1. // ignore_for_file: depend_on_referenced_packages, implementation_imports
  2. import 'dart:async';
  3. import 'package:_fe_analyzer_shared/src/macros/api.dart';
  4. macro class Model implements ClassDeclarationsMacro {
  5. const Model();
  6. static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];
  7. static const _collectionTypes = ['List'];
  8. @override
  9. Future<void> buildDeclarationsForClass(
  10. ClassDeclaration classDeclaration,
  11. MemberDeclarationBuilder builder,
  12. ) async {
  13. final className = classDeclaration.identifier.name;
  14. final fields = await builder.fieldsOf(classDeclaration);
  15. final fieldNames = <String>[];
  16. final fieldTypes = <String, String>{};
  17. final fieldGenerics = <String, List<String>>{};
  18. for (final field in fields) {
  19. final fieldName = field.identifier.name;
  20. fieldNames.add(fieldName);
  21. final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
  22. fieldTypes[fieldName] = fieldType;
  23. if (_collectionTypes.contains(fieldType)) {
  24. final generics = (field.type.code as NamedTypeAnnotationCode)
  25. .typeArguments
  26. .map((e) => (e as NamedTypeAnnotationCode).name.name)
  27. .toList();
  28. fieldGenerics[fieldName] = generics;
  29. }
  30. }
  31. final fieldTypesWithGenerics = fieldTypes.map(
  32. (name, type) {
  33. final generics = fieldGenerics[name];
  34. return MapEntry(
  35. name,
  36. generics == null ? type : '$type<${generics.join(', ')}>',
  37. );
  38. },
  39. );
  40. _buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);
  41. _buildToJson(builder, fieldNames, fieldTypes);
  42. _buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);
  43. _buildToString(builder, className, fieldNames);
  44. _buildEquals(builder, className, fieldNames);
  45. _buildHashCode(builder, fieldNames);
  46. }
  47. void _buildFromJson(
  48. MemberDeclarationBuilder builder,
  49. String className,
  50. List<String> fieldNames,
  51. Map<String, String> fieldTypes,
  52. Map<String, List<String>> fieldGenerics,
  53. ) {
  54. final code = [
  55. 'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
  56. 'return $className('.indent(4),
  57. for (final fieldName in fieldNames) ...[
  58. if (_baseTypes.contains(fieldTypes[fieldName])) ...[
  59. "$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
  60. .indent(6),
  61. ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
  62. "$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
  63. '.whereType<Map<String, dynamic>>()'.indent(10),
  64. '.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
  65. '.toList(),'.indent(10),
  66. ] else ...[
  67. '$fieldName: ${fieldTypes[fieldName]}'
  68. ".fromJson(json['$fieldName'] "
  69. 'as Map<String, dynamic>),'
  70. .indent(6),
  71. ],
  72. ],
  73. ');'.indent(4),
  74. '}'.indent(2),
  75. ].join('\n');
  76. builder.declareInType(DeclarationCode.fromString(code));
  77. }
  78. void _buildToJson(
  79. MemberDeclarationBuilder builder,
  80. List<String> fieldNames,
  81. Map<String, String> fieldTypes,
  82. ) {
  83. final code = [
  84. 'Map<String, dynamic> toJson() {'.indent(2),
  85. 'return {'.indent(4),
  86. for (final fieldName in fieldNames) ...[
  87. if (_baseTypes.contains(fieldTypes[fieldName])) ...[
  88. "'$fieldName': $fieldName,".indent(6),
  89. ] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
  90. "'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),
  91. ] else ...[
  92. "'$fieldName': $fieldName.toJson(),".indent(6),
  93. ],
  94. ],
  95. '};'.indent(4),
  96. '}'.indent(2),
  97. ].join('\n');
  98. builder.declareInType(DeclarationCode.fromString(code));
  99. }
  100. void _buildCopyWith(
  101. MemberDeclarationBuilder builder,
  102. String className,
  103. List<String> fieldNames,
  104. Map<String, String> fieldTypes,
  105. ) {
  106. final code = [
  107. '$className copyWith({'.indent(2),
  108. for (final fieldName in fieldNames) ...[
  109. '${fieldTypes[fieldName]}? $fieldName,'.indent(4),
  110. ],
  111. '}) {'.indent(2),
  112. 'return $className('.indent(4),
  113. for (final fieldName in fieldNames) ...[
  114. '$fieldName: $fieldName ?? this.$fieldName,'.indent(6),
  115. ],
  116. ');'.indent(4),
  117. '}'.indent(2),
  118. ].join('\n');
  119. builder.declareInType(DeclarationCode.fromString(code));
  120. }
  121. void _buildToString(
  122. MemberDeclarationBuilder builder,
  123. String className,
  124. List<String> fieldNames,
  125. ) {
  126. final code = [
  127. '@override'.indent(2),
  128. 'String toString() {'.indent(2),
  129. "return '$className('".indent(4),
  130. for (final fieldName in fieldNames) ...[
  131. if (fieldName != fieldNames.last) ...[
  132. "'$fieldName: \$$fieldName, '".indent(8),
  133. ] else ...[
  134. "'$fieldName: \$$fieldName'".indent(8),
  135. ],
  136. ],
  137. "')';".indent(8),
  138. '}'.indent(2),
  139. ].join('\n');
  140. builder.declareInType(DeclarationCode.fromString(code));
  141. }
  142. void _buildEquals(
  143. MemberDeclarationBuilder builder,
  144. String className,
  145. List<String> fieldNames,
  146. ) {
  147. final code = [
  148. '@override'.indent(2),
  149. 'bool operator ==(Object other) {'.indent(2),
  150. 'return other is $className &&'.indent(4),
  151. 'runtimeType == other.runtimeType &&'.indent(8),
  152. for (final fieldName in fieldNames) ...[
  153. if (fieldName != fieldNames.last) ...[
  154. '$fieldName == other.$fieldName &&'.indent(8),
  155. ] else ...[
  156. '$fieldName == other.$fieldName;'.indent(8),
  157. ],
  158. ],
  159. '}'.indent(2),
  160. ].join('\n');
  161. builder.declareInType(DeclarationCode.fromString(code));
  162. }
  163. void _buildHashCode(
  164. MemberDeclarationBuilder builder,
  165. List<String> fieldNames,
  166. ) {
  167. final code = [
  168. '@override'.indent(2),
  169. 'int get hashCode {'.indent(2),
  170. 'return Object.hash('.indent(4),
  171. 'runtimeType,'.indent(6),
  172. for (final fieldName in fieldNames) ...[
  173. '$fieldName,'.indent(6),
  174. ],
  175. ');'.indent(4),
  176. '}'.indent(2),
  177. ].join('\n');
  178. builder.declareInType(DeclarationCode.fromString(code));
  179. }
  180. }
  181. extension on String {
  182. String indent(int length) {
  183. final space = StringBuffer();
  184. for (var i = 0; i < length; i++) {
  185. space.write(' ');
  186. }
  187. return '$space$this';
  188. }
  189. }

2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救 - 图5

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

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

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

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

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