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
、a2
、b
和c
三个对象,其中a
、a2
、b
都实现了Disposable
接口,都有dispose
方法 - 虽然
a
、a2
、b
和MyState
的dispose();
方法来自不同基类实现,但是基于@AutoDispose()
的实现,在代码调用state.dispose();
时,a
、a2
、b
变量的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
、a2
、b
变量的 dispose
方法,并且它们和 MyState
的 dispose
也来自不同基类,但是最终执行所有 dispose
方法都被成功调用,这就是@AutoDispose()
的宏声明实现在编译时对代码进行了调整。
如下图所示是 @AutoDispose()
的宏编程实现,其中 macro
就是一个标志性的宏关键字,剩下的代码可以看到基本就是 dart 脚本的实现, macro
里主要是实现 ClassDeclarationsMacro
和buildDeclarationsForClass
方法,如下代码可以很直观看到关于 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()
将 GetUsersResponse
和 User
声明为 JSON 对象,然后在运行时,宏编程会自动添加 fromJson
和 toJson
方式。
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 序列化有救
说起宏编程可能大家并不陌生,但是这对于 Flutter 和 Dart 开发者来说它一直是一个「遗憾」,这个「遗憾」体现在编辑过程的代码修改支持上,其中最典型的莫过于 Dart 的 JSON 序列化。 举个例子,目前 Dart 语言的 JSON 序列化高度依…...

云计算概述(云计算类型、技术驱动力、关键技术、特征、特点、通用点、架构层次)(二)
云计算概述(二) (云计算类型、技术驱动力、关键技术、特征、特点、通用点、架构层次) 目录 零、00时光宝盒 一、云计算类型(以服务的内容或形态来分) 二、云计算的12种技术驱动力 三、云计算的关键技术 四、云计…...

物流平台架构设计与实践
随着电商行业的迅猛发展,物流行业也得到了极大的发展。从最初的传统物流到现在的智慧物流,物流技术和模式也在不断的更新与升级。物流平台作为连接电商和物流的重要媒介,其架构设计和实践显得尤为重要。 一、物流平台架构设计 1. 前端架构设…...

RedHat8.4安装邮件服务器
一、配置发件服务器 1.1 根据现场IP,配置主机名 vim /etc/hosts 192.168.8.120 mail.test.com 将主机名更改为邮件服务器域名mail.test.com 1.2 关闭防火墙,禁止开机启动 systemctl stop firewalld systemctl disable firewalld 1.3 关闭selinux v…...

Linux Shell系列--dirname 去除基本文件名
一、目的 上一篇中我们介绍了basename命令的使用,本篇我们介绍dirname命令,dirname 命令与 basename 互补,它负责删除路径中的基本文件名部分(包括扩展名),只保留目录部分。 二、介绍 dirname首先去除字符…...

池化技术的总结
文章目录 1.什么是池化技术2.池化技术的应用一、连接池二、线程池三、内存池 3.池化技术的总结 1.什么是池化技术 池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。 在系统开发过程中,我们经常会用到池化技术。通俗的讲&am…...

H5简约星空旋转引导页源码
H5简约星空旋转引导页源码 源码介绍:一款带有星空旋转背景特效的源码,带有四个按钮 下载地址: https://www.changyouzuhao.cn/11655.html...
前端学习之路(4) vue2和vue3的区别
一. 根节点不同 vue2中必须要有根标签vue3中可以没有根标签,会默认将多个根标签包裹在一个fragement虚拟标签中,有利于减少内存。 二. 组合式API和选项式API 在vue2中采用选项式API,将数据和函数集中起来处理,将功能点切割了当…...

网络原理-TCP/IP(5)
TCP协议 延迟应答 它也是基于滑动窗口,提高效率的一种机制,结合滑动窗口以及流量控制,能够以延迟应答ACK的方式,把反馈的窗口,搞大.核心在于允许范围内,让窗口尽可能大. 如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小. 1.假设接收端缓冲区为1M.一次收到了5…...
Docker 常用命令详细介绍
Docker 是一个开源的应用容器引擎,它允许开发者打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。Docker 使用概率最高的命令…...

10G PON演进到50G PON
10G-PON是万兆无源光网络,光纤链路传输速率能够达到10Gbps。根据ZTE的报告称,截至2023年6月,全球10G PON出货量已超过3000万个PON端口,其中中国市场份额约占80%。 10G PON在中国市场的广泛部署,显着推进了10G PON产业链…...

智能指针——浅析
智能指针 本人不才,只能将智能指针介绍一下,无法结合线程进行深入探索 介绍及作用 在异常产生进行跳转时,通过栈帧回收进行内存释放,防止内存泄漏 基于RAII思想可以创建出只能指针 RAII(Resource Acquisition Is Initializatio…...

JAVA后端上传图片至企微临时素材
1.使用场景 在使用企业微信API接口中,往往开发者需要使用自定义的资源,比如发送本地图片消息,设置通讯录自定义头像等。 为了实现同一资源文件,一次上传可以多次使用,这里提供了素材管理接口:以media_id来…...

MySQL-----初识
一 SQL的基本概述 基本概述 ▶SQL全称: Structured Query Language,是结构化查询语言,用于访问和处理数据库的标准的计算机语言。SQL语言1974年由Boyce和Chamberlin提出,并首先在IBM公司研制的关系数据库系统SystemR上实现。 ▶美国国家标…...

[基础IO]文件描述符{重定向/perror/磁盘结构/inode/软硬链接}
文章目录 1. 再识重定向2.浅谈perror()3.初始文件系统4.软硬链接 1. 再识重定向 图解./sf > file.txt 2>&1 1中内容拷贝给2 使得2指向file 再学一个 把file的内容传给cat cat拿到后再给file2 2.浅谈perror() open()接口调用失败返回-1,并且错误码errno被适当的设置,…...

NAS系统折腾记 – Emby搭建家庭多媒体服务器
Emby简介 Emby是一款优秀的媒体服务器软件,致力于为用户提供丰富的多媒体体验。通过Emby,您可以方便地在家庭内的各种设备上观看您喜爱的电影、电视剧和其他视频内容。而且,Emby还具备强大的媒体管理功能,让您的影视资源井然有序…...

#从零开始# 在深度学习环境中,如何用 pycharm配置使用 pipenv 虚拟环境
为Python项目创建虚拟环境 在深度学习环境和一般python环境中安装pipenv基本一致,只需要确认好pipenv指定的python版本即可,安装pipenv前,可以通过python --version来确认安装版本 快捷键:crtl alt S 查看interpreter,查看所有…...

Cmake编译Opencv3.3.1遇到有些文件无法下载的错误解决:
前言: 对于,opencv有些配置文件错误并未致命,所以,有错误也不影响后续的编译:但是,后引用如果要用,在回过头来还是要解决的。 问题表述: 比如,有些文件下载的错误&am…...

Python基础知识:Python序列以及序列的索引、切片、相乘和相加
索引 索引就是序列中的每个元素所在的位置,可以通过从左往右的正数索引,也可以通过从右往左的负数索引。 从左往右的正数索引:在python序列中,第一个元素的索引值为0,第二个元素的索引值为1,以此类推&…...

回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测
回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测 目录 回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CPO-GRU【24年新算法】冠豪猪优化…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...

使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...