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 开发者来说,无疑是最好的新年礼物。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/250596.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024 年, Web 前端开发趋势

希腊哲学家赫拉克利特认为&#xff0c;变化是生命中唯一不变的东西。这句话适用于我们的个人生活、行业和职业领域。 尤其是前端开发领域&#xff0c;新技术、开发趋势、库和框架不断涌现&#xff0c;变化并不陌生。最近发生的一些事件正在改变开发人员构建网站和 Web 应用的方…

Vue2+ElementUI 弹窗全局拖拽 支持放大缩小

拖拽组件 dialogDrag.vue <template><div></div> </template> <script>export default {name: dialogDrag,data() {return {originalWidth: null,originalHeight: null}},created() {this.$nextTick(()>{this.dialogDrag()})},mounted() {}…

业务流程自动化平台在制造业应用案例,助力业务自动化、智能化

捷昌驱动成立于2000年&#xff0c;并于2018年9月在上海证券交易所上市&#xff0c;是一家专注于线性驱动产品研发、生产及销售的科技集团。 公司整合全球资源&#xff0c;为智慧办公、医疗康护、智能家居、工业自动化等关联产业提供驱动及智能控制解决方案&#xff0c;以科技驱…

一站式在线协作开源办公软件ONLYOFFICE,协作更安全更便捷

1、ONLYOFFICE是什么&#xff1f; ONLYOFFICE是一款功能强大的在线协作办公软件&#xff0c;可以创建编辑Word文档、Excel电子表格&#xff0c;PowerPoint&#xff08;PPT&#xff09;演示文稿、Forms表单等多种文件。ONLYOFFICE支持多个平台&#xff0c;无论使用的是 Windows、…

了解UDP发送过快导致的问题和对应解决方案

在当今这个以数据为核心的时代&#xff0c;企业对于数据传输的速度和稳定性有着日益增长的需求。UDP凭借其低延迟和高效率的特性&#xff0c;在实时通信和大规模数据传输领域扮演着关键角色。然而&#xff0c;UDP的无连接特性和缺乏可靠性也给数据传输带来了挑战&#xff0c;尤…

通过html2canvas和jsPDF将网页内容导出成pdf

jsPDF参考&#xff1a;https://github.com/parallax/jsPDF html2canvas参考&#xff1a;https://github.com/niklasvh/html2canvas 或者 https://html2canvas.hertzen.com 思路 使用html2canvas将选中DOM生成截图对象将截图对象借助jsPDF导出为PDF文件 代码 这是一个示例&a…

MySQL事务隔离级别

1 引言 隔离性在实际操作中比看起来复杂很多。ANSI SQL标准定义了4种隔离级别。如果是数据库领域的新手&#xff0c;强烈建议在阅读特定的MySQL实现之前先熟悉ANSI SQL的通用标准。这个通用标准的目标是定义在事务内外可见和不可见的更改的规则。较低的隔离级别通常允许更高的…

Deepin如何开启与配置SSH实现无公网ip远程连接

文章目录 前言1. 开启SSH服务2. Deppin安装Cpolar3. 配置ssh公网地址4. 公网远程SSH连接5. 固定连接SSH公网地址6. SSH固定地址连接测试 前言 Deepin操作系统是一个基于Debian的Linux操作系统&#xff0c;专注于使用者对日常办公、学习、生活和娱乐的操作体验的极致&#xff0…

计算机网络——链路层(1)

计算机网络——链路层&#xff08;1&#xff09; 小程一言专栏链接: [link](http://t.csdnimg.cn/ZUTXU)前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff0c; [跳转到网站](https://www.captainbed.…

爬虫笔记(三):实战qq登录

咳咳&#xff0c;再这样下去会进橘子叭hhhhhh 以及&#xff0c;这个我觉得大概率是成功的&#xff0c;因为测试了太多次&#xff0c;登录并且验证之后&#xff0c;qq提醒我要我修改密码才可以登录捏QAQ 1. selenium 有关selenium具体是啥&#xff0c;这里就不再赘述了&#x…

Vue-49、Vue技术实现动画效果

1、首先&#xff0c;在Vue项目中的src/components文件夹下创建一个名为AnimatedBox.vue的文件。 2、编辑AnimatedBox.vue文件&#xff0c;添加以下代码&#xff1a; <template><div class"animated-box" click"toggle"><transition name&q…

低代码ERP系统助力企业成本优化,全面解析数字化转型之道!

在企业的日常运营中&#xff0c;成本管理是一项至关重要的任务。随着市场竞争的加剧和业务规模的扩大&#xff0c;传统的成本管理方式往往难以满足现代企业的需求。此时&#xff0c;ERP系统作为一种先进的企业资源管理工具&#xff0c;在成本管理领域展现出巨大的潜力和价值。它…

postman之接口参数签名(js接口HMAC-SHA256签名)

文章目录 postman之接口参数签名&#xff08;js接口签名&#xff09;一、需求背景二、签名生成规则三、postman js接口签名步骤1. postman设置全局、或环境参数2. 配置Pre-request Scripts脚本 四、Pre-request Scripts脚本 常见工作整理1. js获取unix时间戳2. body json字符串…

pdmodel从动态模型转成静态onnx

1.下载项目 git clone https://github.com/jiangjiajun/PaddleUtils.git 2.新建两个新的文件夹 第一个文件夹放两个必要文件 第二个文件夹可以设置为空&#xff0c;用来存放转换后的模型 如图&#xff1a; 3.在终端运行 python paddle/paddle_infer_shape.py --model_dir …

万户 ezOFFICE wpsservlet SQL注入漏洞复现

0x01 产品简介 万户OA ezoffice是万户网络协同办公产品多年来一直将主要精力致力于中高端市场的一款OA协同办公软件产品,统一的基础管理平台,实现用户数据统一管理、权限统一分配、身份统一认证。统一规划门户网站群和协同办公平台,将外网信息维护、客户服务、互动交流和日…

关于可变类型和不可变类型的探究

个人猜想&#xff08;很遗憾失败了&#xff09; 在硬盘或者系统中存在一个字符集 如果存在硬盘中&#xff0c;那么硬盘出厂的时候他的字符集所占用的空间就已经确定了。 如果存在于系统的话&#xff0c;硬盘应该在出厂的时候为系统设置一个存储系统字符集的地方。在安装系统…

List的模拟实现 迭代器

———————————————————— list与vector相比&#xff0c;插入、删除等操作实现的成本非常低&#xff0c;如果在C语言阶段熟悉理解过链表&#xff0c;那么现在实现起来list就显得比较简单&#xff0c;可以说操作层面上比vector更简洁&#xff0c;因为list没有扩…

gRPC使用详解

起源特点主要优缺点应用场景组成部分使用方法SpringBoot集成gRPCVert.x集成gRPCNacos集成gRPC监控gRPC调用过程Java使用示例 起源 gRPC的起源可以追溯到2015年&#xff0c;当时谷歌发布了一款开源RPC框架&#xff0c;名为gRPC。gRPC的设计初衷是为了提供一种标准化、可通用和跨…

概率论中的全概率公式、贝叶斯公式解析

全概率公式 定义 全概率公式是用来计算一个事件的概率&#xff0c;这个事件可以通过几个互斥事件的并集来表示。这几个互斥事件称为“完备事件系”。实质是由原因推结果。 公式 用途 全概率公式通常用于计算一个事件的总概率&#xff0c;特别是当这个事件与几个不同的因素相关…

图书管理系统(ArrayList和LinkedList)--versions3.0

目录 一、项目要求&#xff1a; 二、项目环境 三、项目使用的知识点 四、项目代码 五、项目运行结果 六、项目难点分析 图书管理系统--versions1.0&#xff1a; 图书管理系统--versions1.0-CSDN博客文章浏览阅读981次&#xff0c;点赞29次&#xff0c;收藏17次。本文使用…