资讯动态

da_gen双后端生成实践:Flutter与鸿蒙ArkTS模型代码统一

发布时间:2026/10/21 21:52:24 来源:尧图企业网站定制
上个月在做产品多端模型层设计时我又踩到了那个熟悉的坑Flutter 端用 da_gen 自动生成一批 Data Class DTO鸿蒙侧的 ArkTS 原生模块却还得手写一份几乎一模一样的模型两边字段一多就必定漂移某次联调时一个字段名不一致直接让登录态序列化崩了一晚上。正好那段时间在推进 Flutter 三方库的鸿蒙化适配我就把 da_gen 翻了个底朝天边改边沉淀出了一套双后端生成方案Dart 侧继续走 build_runnerArkTS 侧单独接一个生成后端让同一份模型定义同时产出两套代码。这篇文章就把整个适配过程、关键改造点和端侧 DTO、Immutable 状态管理实战完整记录下来。1. 先把 da_gen 的工作机理吃透再谈适配聊适配之前必须先把 da_gen 本身是怎么转起来的讲清楚。很多人误以为这类库是运行时反射其实恰恰相反。da_gen 是一个典型的构建期代码生成工具它利用了 Dart 的注解和build_runner管道在编译发生之前就把模型代码写好运行时根本不存在解析注解这一步。1.1 注解、build_runner 和生成器三件套是怎么转起来的da_gen 的完整链路可以拆成四段用户在抽象类上打DataClass()注解。build_runner根据build.yaml里声明的 builder 配置找出所有打了注解的源文件。生成器内部通过analyzer包把 Dart 源码解析成语法树从语法树里提取类名、字段、类型、注解参数。最后把这些信息拼接成新的.g.dart文件里面是完整的 Data Class 实现。这段链路里的关键点是第三步。da_gen 并不像某些代码生成库那样靠字符串模板硬拼而是先读 AST再映射成自己的内部结构最后才渲染代码字符串。这意味着它的信息提取层和代码渲染层天然是分离的这为我们后面做鸿蒙化适配省了很大力气。在我接触过的实际项目里大家最喜欢用 da_gen 生成的能力就三种copyWith、和hashCode、toString。尤其copyWith在纯 Dart 的不可变模型场景里几乎是刚需。1.2 生成的产物里到底有什么举个非常典型的例子。我们在项目里定义一个会员信息模型只需要写一个抽象类import package:da_gen/da_gen.dart; DataClass() abstract class MemberProfile { String get memberId; String get nickname; int get level; String? get avatarUrl; ListString get tags; bool get isVip; }da_gen 会自动生成一个完整的MemberProfile实现类包含私有构造、带默认值的工厂构造、copyWith、基于所有字段的和hashCode以及可读性很好的toString。注意这里说的是工厂构造而不是普通的构造函数这正是 da_gen 这类建模工具的价值点它允许你传必填字段的同时让可选字段拥有默认值调用方不需要记参数顺序。class MemberProfile { MemberProfile._({ required this.memberId, required this.nickname, this.level 1, this.avatarUrl, this.tags const [], this.isVip false, }); factory MemberProfile({ required String memberId, required String nickname, int level 1, String? avatarUrl, ListString tags const [], bool isVip false, }) { return MemberProfile._( memberId: memberId, nickname: nickname, level: level, avatarUrl: avatarUrl, tags: tags, isVip: isVip, ); } final String memberId; final String nickname; final int level; final String? avatarUrl; final ListString tags; final bool isVip; MemberProfile copyWith({ String? memberId, String? nickname, int? level, String? avatarUrl, ListString? tags, bool? isVip, }) { return MemberProfile( memberId: memberId ?? this.memberId, nickname: nickname ?? this.nickname, level: level ?? this.level, avatarUrl: avatarUrl ?? this.avatarUrl, tags: tags ?? this.tags, isVip: isVip ?? this.isVip, ); } override bool operator (Object other) { return identical(this, other) || other is MemberProfile other.memberId memberId other.nickname nickname other.level level other.avatarUrl avatarUrl ...; } override int get hashCode Object.hash(memberId, nickname, level, avatarUrl, ...); override String toString() { return MemberProfile(memberId: $memberId, nickname: $nickname, level: $level, avatarUrl: $avatarUrl, isVip: $isVip); } }这段代码看起来不起眼但对一个跨端团队来说它解决了每个开发手写样板代码风格不一致的问题。有人喜欢用const构造有人喜欢写final还有人只比较一半字段到最后全是隐性 bug 源。生成器至少保证了同一批模型的行为完全一致。1.3 构建期生成到底意味着什么构建期三个字在日常交流里经常被忽略但它恰恰是鸿蒙化适配能不能做下去的前提。代码生成发生在构建阶段意味着最终打进应用包里的只是普通 Dart 类不包含source_gen、不包含analyzer、不包含注解处理的任何运行时逻辑。真正要进鸿蒙运行时环境的产物是那些已经生成好的.g.dart文件里的纯 Dart 代码。换句话说da_gen 的依赖可以分成两类依赖生命周期鸿蒙化适配时需要关心的程度analyzer / source_gen / build_runner构建期高决定生成器能否正常跑da_gen 注解库本身的运行时部分构建期 少量编译期中编译进产物时需要兼容鸿蒙 Dart 运行时生成的模型代码运行期低只要语法兼容鸿蒙侧的 Dart 运行时即可这个认知一建立适配边界就清楚了我们不是在鸿蒙设备上运行整个 da_gen而是要让构建期生成工具链能够在鸿蒙应用的构建流程里被调用同时让生成的纯 Dart 代码能通过鸿蒙化 Flutter 运行时的编译与静态检查。2. 鸿蒙化适配的难点不在代码而在构建链很多人一听说鸿蒙化适配就以为要把某个库用 ArkTS 重写一遍这个思路对 UI 组件库可能成立但对 da_gen 这类代码生成器并不适用。真正的问题是构建链的分岔。2.1 鸿蒙 Flutter 运行时给三方库留下的边界先交代一下背景。当前 Flutter 应用要跑在鸿蒙设备上靠的是社区维护的鸿蒙化 Flutter 运行时。这套运行时本质上把 Flutter 引擎和 Dart VM 迁移到了鸿蒙的系统框架上让 Dart 代码可以直接在鸿蒙设备执行。它对外表现和标准 Flutter 引擎基本一致但内部涉及平台通道、渲染层、事件循环的替换。对三方库来说这个适配层的存在意味着两件事纯 Dart 编写的库只要不依赖dart:io里的特殊能力基本可以原样编译进鸿蒙运行时。依赖package:build、package:analyzer这套构建期生态的库能不能跑取决于构建环境而不是设备环境。da_gen 恰好属于构建期重、运行期轻的组合。所以适配的真正工作量不在运行环境而在构建环境。2.2 da_gen 的依赖里哪些需要动手脚我把 da_gen 的依赖逐一列过一遍发现真正需要关注的就三类第一类是build相关依赖比如build_runner、build_config。这些只在构建期起作用鸿蒙运行时根本不会加载它们。但它们对 Dart SDK 版本非常敏感鸿蒙化 Flutter 的 Dart SDK 版本通常比自己引入的 package 版本滞后版本冲突是第一个坑。第二类是analyzer、source_gen。da_gen 靠它们解析 AST、管理生成上下文。这两个库的 API 变化比较频繁鸿蒙化运行时自带的 Dart SDK 如果版本太旧da_gen 的源码可能编译不过。第三类是运行时注解库本身。da_gen 的运行时部分体积不大但一旦进入编译阶段它就要和鸿蒙化 Flutter 引擎里内置的 Dart SDK 一起接受编译检查。只要注解类本身没有用到特殊平台 API这关通常不难过。2.3 两条路线Dart 模型共享还是 ArkTS 代码生成适配路线在第一周讨论时出现了分歧。团队里有人主张直接把 da_gen 的产物在鸿蒙 Flutter 容器里共用这样做工作量最小鸿蒙原生侧单独维护一套轻量模型即可。另一派则认为既然鸿蒙原生侧迟早要做 ArkTS 开发不如把 da_gen 向前推进一步让它能同时生成 ArkTS 模型。最终我们选了偏折中的路线先保证 da_gen 在鸿蒙化 Flutter 构建链路里能稳定工作再把代码生成器改造成双后端架构。也就是同一份中间表示既输出 Dart 的.g.dart也输出 ArkTS 的.ets。之所以这么选是因为鸿蒙化 Flutter 运行时和鸿蒙原生 ArkTS 应用在生态上是两个完全不同的走向。前者短期内能快速复用存量 Dart 代码后者才是长周期内真正要给客户交付的原生形态。双后端一次投入两边都受益。3. 适配实施的关键改造点这一节讲实际操作。我会按我们改造的顺序来写每一步踩了什么坑、为什么这么设计都会说明白。3.1 版本矩阵锁定是第一优先级适配一开始我们做的第一件事不是改代码而是对齐版本。鸿蒙化 Flutter 项目用的 Dart SDK 版本通常由鸿蒙侧适配引擎直接决定项目自己升级 Dart SDK 的余地很小。da_gen 对 analyzer 的版本又有硬性约束两者一旦错位pub get阶段就会爆出一堆难以理解的冲突错误。实际操作中我给项目固定了一个版本矩阵组件版本说明鸿蒙化 Flutter 引擎内置 Dart SDK3.x 线以引擎发布说明为准不能随意升级analyzer6.x 线与引擎内 SDK 匹配通过 dependency_overrides 强制锁定source_gen1.x 线与 analyzer 6.x 配套build_runner2.x 线保持较新但兼容 analyzer如果你的项目引用的 da_gen 版本较新而鸿蒙侧 Dart SDK 较旧最省事的办法不是升级 SDK而是把 da_gen 降一个主版本然后手动改动它内部对 analyzer API 已废弃方法的调用。这个工作我们花了两天但收益很明显之后生成的代码在鸿蒙侧编译时几乎没有再出现过 SDK 版本导致的语法问题。建议在pubspec.yaml里显式加上dependency_overrides: analyzer: 6.4.1 source_gen: 1.4.0这样能有效防止pub get解析出和鸿蒙引擎不兼容的 analyzer 新版本。3.2 提取中间表示IR让 Dart 和 ArkTS 共用一套解析结果da_gen 本身虽然把 AST 解析和代码渲染分开了但它的代码渲染还是为 Dart 定制的。我们要加 ArkTS 后端就得先定义一份与语言无关的中间表示让解析器只在最前面跑一次后面两个后端各取所需。我们定义的 IR 结构大致如下放在lib/src/ir/目录下class ModelIr { final String className; final bool isAbstract; final ListFieldIr fields; const ModelIr({ required this.className, required this.isAbstract, required this.fields, }); } class FieldIr { final String name; final String type; final bool nullable; final bool immutable; final bool hasDefault; final String? defaultValue; final ListString annotations; const FieldIr({ required this.name, required this.type, required this.nullable, this.immutable true, this.hasDefault false, this.defaultValue, this.annotations const [], }); }类型字段先不直接存 Dart 类型字符串而是存一种抽象类型标识比如string、int、list、datetime。后续由各后端把抽象类型映射成目标语言的具体类型。这样 Dart 后端写StringArkTS 后端写string互不干扰。这一步是整个适配的灵魂。没有 IRArkTS 后端就得重新解析一遍 Dart 语法树两套解析逻辑日后必然腐烂。有了 IR我们甚至可以把 IR 序列化成 JSON未来谁想接 Swift 后端、Kotlin 后端只需要前端不变、后端新增一个开关。3.3 新增 ArkTS 代码后端ArkTS 后端是整个改造里最花时间的地方。它要解决的问题比预想的多类型映射、nullability 表达、字符串模板、深层不可变、JSON 序列化的类型安全。一开始我直接用字符串拼 ArkTS 代码后来发现可读性一塌糊涂。最后借鉴了 Dart 后端常见的做法用一组纯函数式的代码块生成器每个块负责一个小片段最后统一拼接。生成出来的 ArkTS 模型长这样export class MemberProfile { readonly memberId: string; readonly nickname: string; readonly level: number; readonly avatarUrl: string | null; readonly tags: Arraystring; readonly isVip: boolean; constructor( memberId: string, nickname: string, level: number 1, avatarUrl: string | null null, tags: Arraystring [], isVip: boolean false, ) { this.memberId memberId; this.nickname nickname; this.level level; this.avatarUrl avatarUrl; this.tags tags; this.isVip isVip; } static create(memberId: string, nickname: string): MemberProfile { return new MemberProfile(memberId, nickname); } copyWith(params: { memberId?: string; nickname?: string; level?: number; avatarUrl?: string | null; tags?: Arraystring; isVip?: boolean; }): MemberProfile { return new MemberProfile( params.memberId ?? this.memberId, params.nickname ?? this.nickname, params.level ?? this.level, params.hasOwnProperty(avatarUrl) ? params.avatarUrl! : this.avatarUrl, params.tags ?? this.tags, params.isVip ?? this.isVip, ); } equals(other: MemberProfile): boolean { return this.memberId other.memberId this.nickname other.nickname this.level other.level this.avatarUrl other.avatarUrl this.tags.length other.tags.length this.tags.every((v, i) v other.tags[i]) this.isVip other.isVip; } hashCode(): number { let h 17; h h * 31 this.memberId.length; h h * 31 this.nickname.length; h h * 31 this.level; h h * 31 (this.avatarUrl ? this.avatarUrl.length : 0); h h * 31 this.tags.length; h h * 31 (this.isVip ? 1 : 0); return h; } toString(): string { return MemberProfile(memberId: ${this.memberId}, nickname: ${this.nickname}, level: ${this.level}, isVip: ${this.isVip}); } }这段代码里最值得注意的就是copyWith对可空字段的处理。Dart 的copyWith用??做回退但 ArkTS 里null和undefined都能表示未传参数如果直接用??那想把某个可空字段显式改成null就永远做不到。我用的方案是hasOwnProperty判断。调用方只要没传avatarUrl这一键就保留旧值一旦显式传了null就真的置空。这个细节不做后面状态管理里清空头像这类操作就会成为深坑。3.4 把生成流程接进鸿蒙项目的构建管线ArkTS 后端单独跑起来很容易但要在真实鸿蒙项目里持续可用必须和构建流程绑定。我们的做法是在鸿蒙工程的构建脚本里加一个前置步骤先用 Dart 写的小工具解析共享模型定义输出.ets文件再交给 IDE 的编译链。共享模型定义放在独立目录Flutter 侧和鸿蒙侧都不直接改模型shared/models/ member_profile.dart order_info.dart login_state.dartFlutter 侧继续用build_runner鸿蒙侧则在构建脚本里执行我们提供的命令行工具dart run da_gen_arkts \ --input shared/models \ --output harmony/entry/src/main/ets/models生成完.ets后IDE 里直接把这些文件当作普通源码参与编译。文件头部我们加了自动生成标记开发同学只要看到这个文件就会去改源定义而不是手工改生成物。这里有个容易被忽略的点dart run需要确保da_gen_arkts这个包被收录在 Flutter 工程的dev_dependencies里鸿蒙工程本身不参与 Dart 包管理。也就是说构建鸿蒙侧时机器上要先有一个可用的 Dart SDK这对 CI 环境是一个额外的环境要求。我们在 CI 流程里专门加了一步避免构建节点上找不到 Dart 命令。4. 端侧 DTO 模型快速构建实战适配完成以后真正每天在用这套机制的是业务开发同学。下面用一个贴近真实的会员模块把从定义到落地的全过程走一遍。4.1 用抽象类 注解定义模型规范端侧 DTO 的核心诉求是字段与接口返回值对齐、类型明确、可空字段可控。我们用 da_gen 的抽象类规范来承载这个诉求文件的命名和目录结构完全对应接口文档的模块。会员模块里我们定义了三个 DTOMemberProfile、MemberOrderSummary、MemberPreference。这里以MemberProfile为例源定义文件里除了抽象类还可以追加一些只是提示生成器如何处理字段的注解import package:da_gen/da_gen.dart; DataClass() abstract class MemberProfile { String get memberId; String get nickname; int get level; Field(immutable: false) String? get avatarUrl; ListString get tags; bool get isVip; DateTime get lastLoginAt; }Field(immutable: false)的含义是avatarUrl更新时不应该触发整个模型的不可变重建它在某些 UI 场景下可以减少无谓的状态复制。这个注解能帮我们微调生成代码而不是所有字段一刀切地final。4.2 Dart 侧生成的工厂构造与序列化方法da_gen 原生并不负责 JSON 序列化但我们的端侧 DTO 绕不开网络层所以适配版扩展到了生成fromJson和toJson。这一步的工作量并不大核心就是按字段类型逐个调用对应的序列化函数。生成出来的工厂构造方法factory MemberProfile.fromJson(MapString, dynamic json) { return MemberProfile( memberId: json[member_id] as String, nickname: json[nickname] as String, level: json[level] as int, avatarUrl: json[avatar_url] as String?, tags: (json[tags] as Listdynamic).castString(), isVip: json[is_vip] as bool, lastLoginAt: DateTime.fromMillisecondsSinceEpoch( json[last_login_at] as int), ); }序列化方向则输出MapString, dynamic toJson() { return { member_id: memberId, nickname: nickname, level: level, avatar_url: avatarUrl, tags: tags, is_vip: isVip, last_login_at: lastLoginAt.millisecondsSinceEpoch, }; }序列化代码最怕手写尤其当接口字段是下划线命名、客户端字段是驼峰命名时手写错一个键名线上就是一次数据丢失。生成器从同一个源定义里倒推映射关系只要源定义正确序列化就稳定一致。4.3 ArkTS 侧的 DTO 模型与类型约束差异ArkTS 侧生成的 DTO 与 Dart 侧在 API 上保持对齐但有个不得不提的差异数值精度。Dart 的int在 VM 里是 64 位整数ArkTS 的number本质上是双精度浮点数安全整数范围只有 53 位。凡是接口里出现id、order_no这类可能超过 9007199254740991 的字段必须改存字符串。我们在 IR 阶段就做了白名单校验类型声明为int且字段名匹配id结尾的会额外生成一个检测日志提醒前端同学将来端侧不要做算术运算。这个提醒在联调阶段救了我们至少三次。4.4 双端一致性的校验手段两边都能生成模型后一致性校验成了新的问题。我们的做法简单但有效在 CI 里同时对同一个shared/models目录跑 Dart 后端和 ArkTS 后端然后比对两份产物的字段名集合。具体实现是一个不到 100 行的脚本读取两端生成物用正则把字段名抽出来做 diff。字段多了少了都会立刻暴露。字段类型的变化则靠人工 code review因为类型映射不是一一对应的。这套校验上线之后跨端模型漂移的问题基本被掐灭在合并请求阶段。5. Immutable 状态管理实战DTO 建模只是工具能力的一半另一半是 Immutable 状态管理。这一节重点说说 da_gen 生成的copyWith和工厂构造是怎么支撑一套简单但不简陋的状态管理方案的。5.1 copyWith 是承载不可变代码的最小杠杆不可变状态管理的核心动作只有两个创建新状态、基于旧状态派生出新状态。前者对应工厂构造后者对应copyWith。没有copyWith每次改字段都要new MemberProfile(...)重新传一遍所有参数代码会膨胀到根本维护不下去。用 da_gen 生成copyWith后业务代码里更新状态就变成了final nextState currentState.copyWith(level: currentState.level 1);这一行代码在鸿蒙侧 ArkTS 里的等价写法是const nextState currentState.copyWith({ level: currentState.level 1 });两端模式完全一致迁移成本趋近于零。5.2 状态容器设计极简 Reducer 风格我们没有上完整的状态管理框架而是用一个极简容器封装不可变状态。容器只提供三个能力当前状态读取、状态更新入口、订阅通知。以登录态为例定义一个不可变状态类DataClass() abstract class LoginState { MemberProfile? get profile; bool get isLoggedIn; String? get pendingError; bool get isLoading; }生成之后配合一个不到 50 行的容器class LoginStore { LoginState _state; final _listeners void Function()[]; LoginStore(this._state); LoginState get state _state; void dispatch(LoginState Function(LoginState current) reducer) { final next reducer(_state); if (next _state) return; _state next; for (final listener in _listeners) { listener(); } } void addListener(void Function() listener) { _listeners.add(listener); } }业务侧更新状态时所有修改都发生在reducer函数里通过copyWith生成新对象然后整体替换。UI 侧不需要关心状态是怎么变的只监听容器通知即可。5.3 一个登录态流转的完整例子场景是用户点击登录按钮。我们把整个登录过程切成三个不可变快照点击前LoginState(isLoading: false, isLoggedIn: false, profile: null)请求中LoginState(isLoading: true, ...)请求成功LoginState(isLoading: false, isLoggedIn: true, profile: MemberProfile(...))请求方法的实现Futurevoid login(String phone, String code) async { store.dispatch( (s) s.copyWith(isLoading: true, pendingError: null)); try { final profile await api.login(phone, code); store.dispatch( (s) s.copyWith(isLoading: false, isLoggedIn: true, profile: profile)); } catch (e) { store.dispatch( (s) s.copyWith( isLoading: false, pendingError: e.toString())); } }每个状态都有明确的不可变对象UI 可以根据isLoading决定是否出 loading根据isLoggedIn决定是否跳转首页根据pendingError决定是否弹提示。联调时这种状态机最大的好处是任何时刻都能通过toString快速打印当前状态快照问题定位时间缩短了一大半。5.4 鸿蒙 ArkTS 侧的实践差异ArkTS 侧的容器和 Dart 侧几乎一一对应但有两个差异值得记录。第一个是集合类型的不可变性。Dart 里List.unmodifiable用起来很方便ArkTS 里我们生成的readonly只保证引用不可变数组内容仍然可改。所以约定是所有从状态类里取出的数组字段都要在 UI 侧先拷贝再使用copyWith生成代码时也要对Array字段做浅拷贝。第二个是监听回调的参数。ArkTS 里如果走了Observed、State这类装饰器状态对象会被框架代理这时如果直接保存旧状态引用做比较结果会失真。我们为了避免和装饰器体系纠缠状态容器全部用普通类 自管理订阅不引入装饰器。这样虽然少了一点框架自动更新能力但状态流转逻辑可预期、可单测。6. 踩坑记录与后续扩展适配过程前后踩了不少坑有些是环境层面的有些是设计层面的。挑四个影响最大的记录在这里。6.1 analyzer 版本冲突导致生成器直接罢工鸿蒙化 Flutter 引擎内置的 Dart SDK 版本偏旧而 da_gen 最新版所依赖的 analyzer API 已经从 6.x 往 7.x 迁移。第一次跑build_runner报错信息有几十行最后定位到是resolver接口签名变化。解决办法有两种要么改用旧版 da_gen要么打补丁。我们选了补丁路线。具体做法是fork一份内部版本把生成器里调用已废弃 API 的地方全部替换为兼容写法。补丁逻辑并不复杂但确实需要逐行阅读生成器源码。平时文档里常说保持依赖更新在鸿蒙化这种特殊构建链里反而是锁死依赖版本更现实。6.2 nullable 字段与 copyWith 的语义冲突这个坑前面提过一次但值得再展开。Dart 的copyWith天然用??合并可空字段一旦想置空就无能为力。适配版里我为 ArkTS 后端设计了hasOwnProperty判断但很快发现 Dart 侧同样有问题。最终的补丁是在 Dart 后端也引入了哨兵值。我们给每个 nullable 字段生成一个私有标记对象copyWith参数如果等于哨兵值表示调用方显式想置空。这个设计让两个后端的行为保持一致后续写状态管理时几乎没有因为语义差异返工。6.3 枚举类型的双端映射没有想象中顺利Dart 的枚举在 ArkTS 里没有天然等价物。我们的第一版方案是映射成字符串常量但业务侧很快抗议后端是数字枚举字符串映射到序列化又要二次转换。第二版改成生成 ArkTS 的联合类型export type MemberTag vip | new_user | pending_review;联合类型在编译期有很好的类型提示但要处理动态值必须写一层守卫函数。这个方案最终用在标签、状态这类有限取值字段上。暂时还没有完美的自动映射碰到复杂枚举还是需要手动在源定义文件旁边补一个 ARKTS 映射声明文件。6.4 后续可以继续扩展的方向双后端架构稳定之后路径已经铺开了。目前最可行的两个扩展方向一是嵌套模型的自动展开。现在遇到ListMemberOrderSummary这类嵌套时我们只对最外层MemberOrderSummary做类型校验内层字段仍靠生成的代码逐层访问。如果能在 IR 阶段做图遍历让嵌套模型也自动进入生成范围跨端模型体系的覆盖度会更高。二是支持从 JSON Schema 反向生成模型定义。端侧 DTO 通常脱胎于接口文档如果能直接喂 OpenAPI 文档给生成工具让源定义文件本身也成为中间产物那开发人员连抽象类都不用手写了接口变更时模型自动对齐。这个方向我们已经在一部分内部工具链上做了原型等稳定之后单独写一篇分享。最后分享一个我在实战里养成的小习惯生成代码永远不要手工改。团队里凡是有人直接在.g.dart或.ets文件里动了字段迟早会在下一次重建时被无情覆盖。正确流程永远是改shared/models下的源定义然后重新生成、检查 diff、提交。坚持这个约定双端模型维护的成本会低到可以忽略整套适配方案的价值才能真正释放出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑