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_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-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_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'];

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP集成开发 -- PhpStorm 2023

PhpStorm 2023是一款强大的PHP集成开发环境&#xff08;IDE&#xff09;&#xff0c;旨在提高开发人员的生产力和代码质量。以下是关于PhpStorm 2023软件的详细介绍&#xff1a; 首先&#xff0c;PhpStorm 2023提供了丰富的代码编辑功能&#xff0c;包括语法高亮、自动补全、代…

计算机网络自顶向下Wireshark labs-HTTP

我直接翻译并在题目下面直接下我的答案了。 1.基本HTTP GET/response交互 我们开始探索HTTP&#xff0c;方法是下载一个非常简单的HTML文件 非常短&#xff0c;并且不包含嵌入的对象。执行以下操作&#xff1a; 启动您的浏览器。启动Wireshark数据包嗅探器&#xff0c;如Wir…

版本管理git及其命令介绍-附带详细操作

前言 在版本管理时代之前&#xff0c;人们写软件的方式如下图1所示 图1 无版本管理的代码 其坏处就是软件版本随着时间越来越多&#xff0c;每个版本修改了什么内容&#xff0c;修改了哪些文件&#xff0c;如果没有详细记录也不知道。这样久会导致如果我们想回退到某个版本内…

LLM(3) | 自注意力机制 (self-attention mechanisms)

LLM(3) | 自注意力机制 (self-attention mechanisms) self-attention 是 transformer 的基础&#xff0c; 而 LLMs 大语言模型也都是 transformer 模型&#xff0c; 理解 self-attention, 才能理解为什么 LLM 能够处理好上下文关联性。 本篇是对于 Must-Read Starter Guide t…

Java学习day26:和线程相关的Object类的方法、等待线程和唤醒线程(知识点详解)

声明&#xff1a;该专栏本人重新过一遍java知识点时候的笔记汇总&#xff0c;主要是每天的知识点题解&#xff0c;算是让自己巩固复习&#xff0c;也希望能给初学的朋友们一点帮助&#xff0c;大佬们不喜勿喷(抱拳了老铁&#xff01;) 往期回顾 Java学习day25&#xff1a;守护线…

(十二)常见Linux命令——磁盘分区、进程线程、系统定时任务

文章目录 1、磁盘分区类命令1.1、df (disk free 空余硬盘)查看磁盘空间使用情况1.2、fdisk 查看分区1.3、mount/umount 挂载/卸载 2、进程线程类命令2.1、ps (process status 进程状态)查看当前系统进程状态2.2、kill终止进程 3、系统定时任务命令3.1、crond服务管理3.2、cront…

使用ngrok内网穿透

没有服务器和公网IP&#xff0c;想要其他人访问自己做好的网站&#xff0c;使用这款简单免费的内网穿透小工具——ngrok&#xff0c;有了它轻松让别人访问你的项目~ 一、下载ngrok 官网地址&#xff1a;ngrok | Unified Application Delivery Platform for Developers&#x…

前端开发者应该知道的TypeScript可区分联合

作为一个前端开发者&#xff0c;你的工作不仅仅是移动像素&#xff0c;前端的大部分复杂性来自于处理你的应用程序可能处于的所有不同状态。 它可能是加载数据&#xff0c;等待表单被填写&#xff0c;或者发送一个遥测事件 - 或者同时进行这三项。 如果不能正确处理状态&…

【PostgreSQL内核学习(二十五) —— (DBMS存储空间管理)】

DBMS存储空间管理 概述块&#xff08;或页面&#xff09;PageHeaderData 结构体HeapTupleHeaderData 结构 表空间表空间的作用&#xff1a;表空间和数据库关系表空间执行案例 补充 —— 模式&#xff08;Schema&#xff09; 声明&#xff1a;本文的部分内容参考了他人的文章。在…

深度学习入门笔记(七)卷积神经网络CNN

我们先来总结一下人类识别物体的方法: 定位。这一步对于人眼来说是一个很自然的过程,因为当你去识别图标的时候,你就已经把你的目光放在了图标上。虽然这个行为不是很难,但是很重要。看线条。有没有文字,形状是方的圆的,还是长的短的等等。看细节。纹理、颜色、方向等。卷…

C++学习Day01之namespace命名空间

目录 一、程序及输出1.1 命名空间用途&#xff1a; 解决名称冲突1.2 命名空间内容1.3 命名空间必须要声明在全局作用域下1.4 命名空间可以嵌套命名空间1.5 命名空间开放&#xff0c;可以随时给命名空间添加新的成员1.6 命名空间可以是匿名的1.7 命名空间可以起别名 二、分析与总…

洛谷 P1980 [NOIP2013 普及组] 计数问题

题目背景 NOIP2013 普及组 T1 题目描述 试计算在区间 1 到 n 的所有整数中&#xff0c;数字 x&#xff08;0≤x≤9&#xff09;共出现了多少次&#xff1f;例如&#xff0c;在 1 到 11 中&#xff0c;即在 1,2,3,4,5,6,7,8,9,10,11 中&#xff0c;数字 1 出现了 4 次。 输入…

基于Java SSM框架实现校园快领服务系统项目【项目源码+论文说明】

基于java的SSM框架实现校园快领服务系统演示 摘要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于校园快领服务系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了…

Electron+Vue3+Vite的产品级模板项目

1. electron-vue3-template 基于Vue3 Electron TypeScript的客户端程序模板&#xff0c;使用Vite和Electron Forge构建和打包。 真正做到开箱即用&#xff0c;面向跨平台客户端设计&#xff0c;产品级的项目模板。 项目地址&#xff1a; https://github.com/winsoft666/el…

Golang `crypto/hmac` 实战指南:代码示例与最佳实践

Golang crypto/hmac 实战指南&#xff1a;代码示例与最佳实践 引言HMAC 的基础知识1. HMAC 的工作原理2. HMAC 的应用场景 Golang crypto/hmac 库概览1. 导入和基本用法2. HMAC 的生成和验证3. crypto/hmac 的特性 实战代码示例示例 1: 基本的 HMAC 生成示例 2: 验证消息完整性…

C++通用编程(2)

函数模板高级用法 1.分文件编写的优点2.普通函数的分文件编写3.函数模板的分文件编写4.细节提示5.函数模板应用高级decltype推导类型函数后置返回类型 6.总结 函数模板讲完后&#xff0c;C全部的函数类型我们就接触的差不多了。今天给做一些关于函数份文件编写的知识点补充。 1…

C语言问题汇总

指针 #include <stdio.h>int main(void){int a[4] {1,2,3,4};int *p &a1;int *p1 a1;printf("%#x,%#x",p[-1],*p1);} 以上代码中存在错误。 int *p &a1; 错误1&#xff1a;取a数组的地址&#xff0c;然后1&#xff0c;即指针跳过int [4]大小的字节…

调试以及发布npm组件

开发原因&#xff1a; 由于公司自己的封装到npm的组件有点问题&#xff0c;负责人由在忙其他&#xff0c;就由我去负责改改&#xff0c;中途出了不少问题&#xff0c;记录一下。 一、下载源码 第一步肯定是去git上把组件的源码下载下来&#xff0c;这一步没什么好说&#xf…

日志记录——单片机可执行文件合并

一&#xff1a;需求场景 现在有一片单片机&#xff0c;执行程序包括自定义boot和应用程序app, 在将打包好的固件给到生产是有以下问题&#xff0c;由于要通过jlink烧录boot&#xff0c;然后上电启动boot&#xff0c;通过boot烧录初始化程序&#xff0c;过程过于复杂&#xff0…

Oracle和Mysql数据库

数据库 Oracle 体系结构与基本概念体系结构基本概念表空间(users)和数据文件段、区、块Oracle数据库的基本元素 Oracle数据库启动和关闭Oracle数据库启动Oracle数据库关闭 Sqlplussqlplus 登录数据库管理系统使用sqlplus登录Oracle数据库远程登录解锁用户修改用户密码查看当前语…