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

本文主要是介绍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 的序列化和反序列化

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

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

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

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

  • MyState 添加了一个自定义的 @AutoDispose() 注解,这是一个开发者自己实现的宏声明,并且继承了 State 对象,带有 dispose 方法。
  • MyState 里有多个 a a2bc 三个对象,其中 a a2b 都实现了 Disposable 接口,都有 dispose 方法
  • 虽然 a a2bMyStatedispose(); 方法来自不同基类实现,但是基于 @AutoDispose() 的实现,在代码调用 state.dispose(); 时, a a2b 变量的 dispose 方法也会被同步调用
import 'package:macro_proposal/auto_dispose.dart';void main() {var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');state.dispose();
}()
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});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 没用主动调用 a a2b 变量的 dispose 方法,并且它们和 MyStatedispose 也来自不同基类,但是最终执行所有 dispose 方法都被成功调用,这就是@AutoDispose() 的宏声明实现在编译时对代码进行了调整。

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

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();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();',]));}Future<void> buildDefinitionForClass(ClassDeclaration clazz, TypeDefinitionBuilder builder) async {var disposableIdentifier =// ignore: deprecated_member_useawait 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-language 的 macros/example/auto_dispose_main ,其中 bin/ 目录下的代码是运行的脚本示例,lib/ 目录下的代码是宏编程实现的示例:

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

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

  • 需要 dart sdk 3.4.0-97.0.dev ,目前你可以通过 master 分支下载这个 dark-sdk https://storage.googleapis.com/dart-archive/channels/main/raw/latest/sdk/dartsdk-macos-arm64-release.zip

  • 将 sdk 配置到环境变量,或者进入到 dart sdk 的 bin 目录执行 ./dart --version 检查版本

  • 进入上诉的 example 下执行 dart pub get,过程可能会有点长

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

另外,还有一个第三方例子是来自 millsteed 的 macros ,这是一个简单的 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 方式。

import 'dart:convert';import 'package:macros/model.dart';()
class User {User({required this.username,required this.password,});final String username;final String password;
}()
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 是实现调整到宏实现了,而最终效果就是,开发者使用起来更加优雅了。

// ignore_for_file: depend_on_referenced_packages, implementation_importsimport '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'];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 开发者来说,无疑是最好的新年礼物。

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

这篇关于2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/674700

相关文章

定价129元!支持双频 Wi-Fi 5的华为AX1路由器发布

《定价129元!支持双频Wi-Fi5的华为AX1路由器发布》华为上周推出了其最新的入门级Wi-Fi5路由器——华为路由AX1,建议零售价129元,这款路由器配置如何?详细请看下文介... 华为 Wi-Fi 5 路由 AX1 已正式开售,新品支持双频 1200 兆、配有四个千兆网口、提供可视化智能诊断功能,建

轻松上手MYSQL之JSON函数实现高效数据查询与操作

《轻松上手MYSQL之JSON函数实现高效数据查询与操作》:本文主要介绍轻松上手MYSQL之JSON函数实现高效数据查询与操作的相关资料,MySQL提供了多个JSON函数,用于处理和查询JSON数... 目录一、jsON_EXTRACT 提取指定数据二、JSON_UNQUOTE 取消双引号三、JSON_KE

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

SpringBoot定制JSON响应数据的实现

《SpringBoot定制JSON响应数据的实现》本文主要介绍了SpringBoot定制JSON响应数据的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录前言一、如何使用@jsonView这个注解?二、应用场景三、实战案例注解方式编程方式总结 前言

C#多线程编程中导致死锁的常见陷阱和避免方法

《C#多线程编程中导致死锁的常见陷阱和避免方法》在C#多线程编程中,死锁(Deadlock)是一种常见的、令人头疼的错误,死锁通常发生在多个线程试图获取多个资源的锁时,导致相互等待对方释放资源,最终形... 目录引言1. 什么是死锁?死锁的典型条件:2. 导致死锁的常见原因2.1 锁的顺序问题错误示例:不同

JSON字符串转成java的Map对象详细步骤

《JSON字符串转成java的Map对象详细步骤》:本文主要介绍如何将JSON字符串转换为Java对象的步骤,包括定义Element类、使用Jackson库解析JSON和添加依赖,文中通过代码介绍... 目录步骤 1: 定义 Element 类步骤 2: 使用 Jackson 库解析 jsON步骤 3: 添

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言

四种Flutter子页面向父组件传递数据的方法介绍

《四种Flutter子页面向父组件传递数据的方法介绍》在Flutter中,如果父组件需要调用子组件的方法,可以通过常用的四种方式实现,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录方法 1:使用 GlobalKey 和 State 调用子组件方法方法 2:通过回调函数(Callb

IDEA如何将String类型转json格式

《IDEA如何将String类型转json格式》在Java中,字符串字面量中的转义字符会被自动转换,但通过网络获取的字符串可能不会自动转换,为了解决IDEA无法识别JSON字符串的问题,可以在本地对字... 目录问题描述问题原因解决方案总结问题描述最近做项目需要使用Ai生成json,可生成String类型

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1