ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ferry_generator 鸿蒙 Flutter 适配实践:数据层自动化迁移指南

ferry_generator 鸿蒙 Flutter 适配实践:数据层自动化迁移指南 做 Flutter 开发这几年GraphQL 项目里ferry_generator基本是我数据层不可缺少的生成器。最近把一套基于 ferry 的跨端工程往**鸿蒙HarmonyOS**迁移很多朋友第一反应都是“生成器是纯 Dart 工具鸿蒙上应该直接就能跑”真正上手之后才发现问题全藏在依赖收敛、网络层实现和构建链路这些细节里。这篇文章会把 ferry_generator 在鸿蒙 Flutter 工程里的适配路径完整梳理一遍包括工具链版本怎么选、build.yaml 怎么配、生成后的数据层怎么接、HTTP 和 WebSocket 在鸿蒙上要改哪些地方以及我踩过的坑和排查手段给正打算把数据层自动化这套方案搬到鸿蒙生态的团队一个可以直接复用的参考。1. 项目背景与鸿蒙化适配的整体思路1.1 ferry_generator 到底解决了什么问题先说清楚 ferry_generator 在整条数据链路里的位置。ferry 是 Flutter/Dart 生态里一套完整的 GraphQL 客户端方案功能和前端的 Apollo 客户端对标负责请求发送、缓存管理、错误处理和响应分发。ferry_generator 则是配套的代码生成器它做的事情可以理解成“把 GraphQL 世界里那些手写起来费劲、出错率又高的样板代码在编译阶段全部自动生成”。具体来说我们只需要维护两样东西一份描述后端接口类型的schema.graphql以及若干定义 query、mutation、subscription 的.graphql文件。跑一遍build_runnerferry_generator 就会生成对应的 Dart 类包括操作请求类、变量参数类、返回数据类、fragment 类型类以及序列化反序列化逻辑。没有这套工具时从 JSON 到模型再到请求参数的搬运都是手写后端字段一改所有调用点全部跟着手工改有了生成器之后改完 schema 和 graphql 文件再重新生成所有类型错误直接暴露在编译期。这套方案的另一个价值是运行时性能。生成的代码是普通 Dart 类运行时不需要解析 GraphQL 字符串数据类型在编译期就已经确定对鸿蒙这种偏 AOT 编译、追求启动性能的环境来说反而更友好。也就是说ferry_generator 本身是一套非常值得保留的基础设施鸿蒙化适配的难点不在生成器本身而在生成后的代码要跑在鸿蒙运行时上所需要的那些“周边能力”。1.2 鸿蒙 Flutter 生态里的适配切入口鸿蒙的 Flutter SDK 与上游开源 Flutter 之间不是简单的一对一关系。鸿蒙侧维护了一条自己的 Flutter 分支捆绑的 Dart SDK 版本、底层引擎实现、插件通信方式都和标准 Flutter 有差异。因此“鸿蒙化适配”通常不是一个一刀切的概念而是按依赖层拆开看纯 Dart 层不依赖任何平台通道和原生能力理论上只要 Dart SDK 版本兼容就能跑。插件原生层依赖 Android/iOS 或鸿蒙系统 API这一层在鸿蒙上必须寻找或编写对应的 ohos 实现。网络与存储层虽然 Dart 层面有统一的接口但底层 socket、文件路径、后台任务行为和平常使用的平台实现不同最容易出冷门问题。ferry_generator 属于第一类ferry 的多数核心逻辑也属于第一类但 ferry 默认使用的 HTTP Link 底层依赖的是package:httpWebSocket subscription 底层依赖web_socket_channel这两个包在鸿蒙上的表现就成了适配的主要变量。所以我把这次适配的切入口定在“生成链路不变、网络层可替换、构建链路可控”这三个方向上。1.3 三条适配路线怎么选在动手之前我列了三种方案也建议团队先想清楚自己的约束条件再选方案 A版本收敛直接跑通。保持 ferry 全家桶不变只处理依赖版本与鸿蒙 Flutter SDK 的兼容问题。适合业务只用 query/mutation、不依赖 subscription、后端接口简单的场景。这个方案改动量最小但一旦在鸿蒙上遇到底层网络问题排查边界会比较大因为package:http在鸿蒙上的行为不完全可控。方案 B生成器不变替换网络层。保留 ferry_generator 和 ferry 的缓存逻辑把默认的 HttpLink/WebSocketLink 换成基于 dio 或鸿蒙原生网络能力的自定义 Link。这是我最推荐的做法生成器的价值保住了网络层这个最容易出问题的点变成我们可以控制的部分。方案 C深度自研客户端。连 ferry 都放弃只用生成器输出类型定义自己实现一个轻量请求层。除非业务对包体积和启动时间敏感到极致否则不建议ferry 的缓存、错误处理、分页逻辑如果全部重写工作量远超省下的那点依赖体积。我最终选择的是方案 B后面所有实操内容也围绕这个路线展开。2. 适配前置准备环境、目录与依赖改造2.1 工具链版本对照鸿蒙 Flutter 开发目前需要两套工具链配合使用一套是鸿蒙版的 Flutter SDK另一套是 DevEco Studio。这里有个非常容易踩的坑鸿蒙版 Flutter SDK 绑定的 Dart 版本通常比标准 Flutter 最新版低不少而 ferry 相关依赖包在 pub 上不断升级新版本代码可能用到较新语法或新 API解析出来的依赖树在鸿蒙 SDK 的 Dart 编译器上可能直接过不去。我这次验证使用的组合大致如下大家以自己实际拿到 SDK 版本为准组件说明鸿蒙 Flutter SDK从鸿蒙开发者官方渠道获取带有 harmony 分支标识DevEco Studio用于创建鸿蒙壳工程、配置签名、跑模拟器和真机Dart SDK跟随鸿蒙 Flutter SDK 自动捆绑单独覆盖 Dart 版本风险很高pub 依赖ferry、ferry_generator、gql、graphql_codegen、dio建议在一开始就把flutter --version输出截图或存下来后面所有依赖版本锁定都以这条 Dart 版本基线为准。别直接拿标准 Flutter 的 pubspec.lock 拷过来我在迁移时吃过这个亏后面在 4.1 节会详细说。2.2 鸿蒙壳工程与 Flutter module 的目录关系鸿蒙工程和 Flutter 工程的集成关系和 Android 侧类似但目录形态更接近“壳工程包裹 Flutter module”。整个工程里鸿蒙原生代码在ohos目录下Flutter 业务代码在lib目录下pubspec.yaml位于 Flutter module 根目录。理解这层结构的重要性在于build_runner 生成代码时读写的是 Flutter module 自己的文件系统而运行时权限和网络能力声明则要写在鸿蒙壳工程里。两个工程之间通过模块依赖关联但pubspec.yaml的依赖解析、.dart_tool缓存都只作用在 Flutter 侧。我习惯的目录组织方式如下your_project/ ├── ohos/ # 鸿蒙壳工程 │ ├── entry/ │ │ └── src/main/ │ │ ├── module.json5 │ │ └── ets/ ├── lib/ │ ├── data/ │ │ ├── ferry_client.dart │ │ ├── graphql/ │ │ │ ├── schema.graphql │ │ │ ├── home.graphql │ │ │ └── user.graphql │ │ └── *.graphql.dart # 生成产物 │ ├── pages/ │ └── main.dart ├── pubspec.yaml └── build.yaml这样组织的好处是数据层全部收敛在data目录下生成产物和手写代码分区明确后续做代码审查或者把生成文件加入 gitignore 都很方便。2.3 pubspec.yaml 依赖改造与版本收敛策略依赖改造是这次适配里第一个真正的坑。我最初的请求方式是直接把原本运行在标准 Flutter 上的 ferry 依赖版本原封不动迁到鸿蒙工程结果flutter pub get倒是过了dart run build_runner build时出现一堆 analyzer 和 build 框架的版本冲突。经过几轮调整pubspec.yaml 里与 ferry 相关的关键部分大致长这样版本号是示例具体以 pub 为准dependencies: flutter: sdk: flutter ferry: ^0.16.0 gql: ^0.14.0 gql_exec: ^1.0.0 gql_link: ^1.0.0 gql_dio_link: ^1.0.0 dio: ^5.0.0 hive: ^2.2.3 hive_flutter: ^1.1.0 dev_dependencies: build_runner: ^2.4.0 ferry_generator: ^0.9.0 graphql_codegen: ^0.14.0有几个值得注意的点dio 尽量选择鸿蒙社区有适配验证的版本。dio 在 Flutter 生态里因为网络栈可控、拦截器机制完善是很多鸿蒙三方库适配的首选 HTTP 客户端这也是我最终把网络层换成gql_dio_link的原因之一。build_runner 和 analyzer 的版本不能无脑升级。鸿蒙 Flutter SDK 捆绑的 Dart 版本决定了 analyzer 的上限建议先把标准 Flutter 工程里跑通的 build_runner 版本照搬过来再做增量升级。hive 和 hive_flutter 用于 ferry 缓存持久化。ferry 的内存缓存跑起来没问题但如果要做离线缓存需要一套鸿蒙上能正常运行的数据存储方案hive 属于纯 Dart 少量平台通道实现适配成本相对低。如果依赖解析还是出现无法调和的冲突可以加 dependency_overrides 强制指定某个包版本但每次 override 都要在注释里写清楚原因避免团队其他成员后续接手时一头雾水。2.4 module.json5 网络权限配置鸿蒙侧的网络权限声明在ohos/entry/src/main/module.json5里。GraphQL 请求走 HTTPS订阅走 WebSocket需要在请求权限数组里加上ohos.permission.INTERNET。这个配置我漏掉过一次现象非常迷惑应用冷启动时可以走通 HTTP 请求但一旦切到 WebSocket 订阅就立即失败日志提示网络权限不足。鸿蒙网络权限没有运行时申请流程纯靠配置文件预声明漏了就只能重新打包。{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET } ] } }如果后续还要处理本地缓存文件读写同步检查一下文件存储相关权限否则 hive 在鸿蒙上初始化时可能抛出HiveError: Unable to open directory这类异常。3. 核心适配技术与数据层实现3.1 build.yaml 生成器配置实战build.yaml 是 graphql_codegen 的配置入口配置不对会直接影响生成结果。我在这台鸿蒙工程里用的配置如下targets: $default: builders: graphql_codegen: options: clients: - name: FerryClient path: lib/data/ferry_client.dart scalars: DateTime: type: DateTime import: package:ferry/type_helpers.dart JSON: type: MapString, dynamic import: dart:convert output: extension: .graphql.dart这里有一个非常关键的细节clients配置告诉生成器在生成的代码里导入我们自己定义的 FerryClient 类路径要写对。如果只生成数据类型而不生成 client 相关的 helper后面调用ferryClient.request(...)时的类型推断体验会差很多。scalars的作用是把 GraphQL 里的自定义标量映射成 Dart 类型。后端如果有自定义的时间标量、JSON 标量一定要在这里配置好否则生成代码里全是dynamic等于放弃了强类型的核心价值。我在第一次生成时遇到的典型错误是Could not find a file named schema.graphql。原因是 schema 文件没有放在 lib 目录下build.yaml 的默认 targets 只扫描 lib 下的文件。把schema.graphql放到lib/data/graphql/之后问题消失。3.2 Schema 与 .graphql 文件组织规范操作文件组织有几条经验可以分享一个模块一个文件。比如首页相关的 query 集中在home.graphql用户中心集中在user.graphql避免所有 query 堆在一个文件里导致生成产物过大。fragment 单独抽出来。GraphQL fragment 对应的生成类可以在多个 query 间复用建议放在fragments.graphql里统一管理命名上统一加前缀比如UserBasicFragment。给操作起稳定的名字。生成类名直接来自操作名比如query Pet($id: ID!)会生成GPetQuery一旦发布到线上再改名所有 import 和调用点都得跟着改所以命名要慎重。示例操作文件# lib/data/graphql/home.graphql query Pet($id: ID!) { pet(id: $id) { id name owner { id } } } fragment PetBasic on Pet { id name }执行生成命令dart run build_runner build --delete-conflicting-outputs不加--delete-conflicting-outputs时如果生成器输出文件和旧版本冲突build 会直接中断提示手动删除。加了之后会自动用新生成结果覆盖旧文件日常开发效率高很多。CI 环境里第一次拉代码后也要先跑一遍生成命令保证提交到仓库的生成代码和源文件一致。3.3 生成代码剖析与 FerryClient 初始化生成完成后home.graphql.dart里会包含GPetQuery、GPetQueryArguments、GPetQueryData、GPetQueryData_pet这些类。以GPetQuery的调用为例import package:ferry/ferry.dart; import package:gql_dio_link/gql_dio_link.dart; import package:dio/dio.dart; import ../graphql/home.graphql.dart; final dio Dio( BaseOptions( connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 15), ), ); final client FerryClient( link: Link.from([ DioLink(https://api.example.com/graphql, client: dio), ]), cache: Cache(), ); StreamOperationResponseGPetQueryData, GPetQueryAllVars requestPet(String id) { return client.request(GPetQuery( (b) b..vars.id id, )); }这里生成的GPetQuery是发起请求的入口对象b..vars.id id这种 Builder 风格是 ferry 的既定写法熟悉后会觉得比传一个命名参数 Map 安全得多。返回值是一个Stream每个事件对应一次OperationResponse里面包含loading、data、linkException、error等字段。在页面里消费这个流时我习惯用await for或StreamBuilder而非FutureBuilder原因后面会讲。3.4 鸿蒙网络层替换放弃默认 HttpLink 的时机与方案这是整个适配过程中最核心的改造点。ferry 默认使用的 HttpLink 基于package:http的 IOClient在鸿蒙 Flutter SDK 上dart:io的 socket 行为经过引擎层二次封装后可能出现连接超时、TLS 握手异常这类冷门问题。我的判断是鸿蒙上不要花时间去验证默认 HttpLink 是否“看起来正常”直接换成可自主控制的网络栈。我采用的替换方案是gql_dio_link它基于gql_link标准接口把 Dio 作为底层 HTTP 客户端。相比自己实现一个 Link这种现成桥接库的改动量最小。核心代码如下import package:gql_dio_link/gql_dio_link.dart; import package:dio/dio.dart; final dio Dio( BaseOptions( baseUrl: https://api.example.com/graphql, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 15), ), ); final link Link.from([ DioLink(https://api.example.com/graphql, client: dio), ]);Dio 在鸿蒙上的适配成熟度比package:http好很多主要体现在连接管理、超时控制和日志拦截上。日常调试时加上 Dio 的LogInterceptor可以直接在控制台看到完整的请求体、响应体和耗时排查 GraphQL 查询语法错误会轻松很多。如果项目里 subscription 是硬需求WebSocketLink 部分也建议做一层桥接。web_socket_channel包在鸿蒙上的兼容性需要单独验证我遇到的问题是握手成功后偶发断连。可以用鸿蒙原生 WebSocket 能力封装一个满足gql_link接口的 Link或者退一步在鸿蒙上先轮询替代订阅同时推动后端提供 REST 兜底接口。3.5 错误处理与缓存接入要点ferry 的OperationResponse分类方式比较清晰我在适配时统一封装了三个处理方法void handleResponse(OperationResponse response) { if (response.loading) { // 显示 loading 状态 return; } if (response.linkException ! null) { // 网络层异常提示重试 return; } if (response.error ! null) { // GraphQL 业务错误展示 error 信息 return; } if (response.data ! null) { // 正常渲染数据 return; } }注意linkException和error的区别前者是网络错误、超时、TLS 异常这类底层问题后者是服务端 GraphQL 执行层返回的业务错误。在小程序、App 前端开发里经常被笼统处理但 ferry 把它们拆开了建议在鸿蒙项目里也保持这种区分后续埋点和排查会非常方便。缓存方面ferry 默认的Cache()是内存缓存重启应用后消失。如果业务需要离线可用可以换成Cache(save: ..., read: ...)同时接入 hive 做持久化。hive 在鸿蒙上初始化时要小心目录问题建议初始化时明确指定一个应用沙盒路径不要依赖相对路径。4. 实操过程与完整复现4.1 从创建工程到首次请求成功下面是我在鸿蒙工程上跑通 ferry_generator 全链路的完整步骤每一步后面附带我当时实际操作中遇到的问题第一步创建鸿蒙壳工程并在其中集成 Flutter module。用 DevEco Studio 新建工程然后在工程目录下初始化 Flutter module。注意鸿蒙 Flutter SDK 的环境变量要提前配好flutter doctor检测时出现的任何红叉都要先解决否则后面每次执行 dart 命令都会在诡异的地方报错。第二步收敛依赖版本。新建 pubspec.yaml 后先把 ferry、ferry_generator、gql_dio_link、dio 加入依赖执行flutter pub get。执行成功后立刻检查 pubspec.lock确认所有包解析的版本都落在鸿蒙 Dart SDK 支持范围内。这一步最容易在深处出问题常见情况是某个传递依赖的新版使用了不可用的语法导致编译生成器时直接报错。如果出现就给该包加 dependency_overrides 降级。第三步配置 build.yaml 和 schema。按 3.1 节写好 build.yaml把schema.graphql放到lib/data/graphql/下。schema 文件可以直接从后端 Apollo 或 GraphQL 网关导出导出时带上全部 type 定义。第四步执行代码生成。dart run build_runner build --delete-conflicting-outputs首次生成时间会比较长几秒到几十秒不等取决于 schema 规模和机器性能。成功后检查home.graphql.dart是否生成并查看生成的类名是否符合预期。第五步初始化 FerryClient 并接入页面。在lib/data/ferry_client.dart里创建全局单例 client然后写一个简单的查询调用跑一个最小页面验证。第六步编译到鸿蒙模拟器或真机。在 DevEco Studio 里配置好签名后直接 run。首次跑起来会遇到一个和 Android 工程类似的问题Flutter module 作为依赖被鸿蒙工程构建时生成代码路径可能存在缓存需要清一下build目录再编译。4.2 真机联调回顾与日志分析真机联调时我抓到的第一个典型问题是 HTTP 请求 200 返回但 FerryClient 一直拿不到数据。排查后发现是 Dio 的validateStatus默认行为导致的GraphQL 很多服务端错误会返回 400 或 500 状态码Dio 默认把非 2xx 状态一律视为异常但 ferry 期望的是把响应体内容交给linkException逻辑去判断。解决办法是给 Dio 配置宽松的 status 校验dio.options.validateStatus (status) status ! null status 500;第二个典型问题出现在订阅场景。WebSocket 握手成功后ferry 的监听流始终没有数据推送。排查思路是先确认鸿蒙连接器是否具备后台权限再确认服务端推送格式是否符合gql协议约定。由于订阅逻辑在鸿蒙上绕不开 WebSocket 通道我最终采用了一个更加保守的策略在鸿蒙版本里先关闭 subscription 入口改为短轮询 HTTP 查询稳定后再单独投入精力做 WebSocket 通道桥接。4.3 性能、包体积与增量构建实测ferry_generator 生成的代码量不小一个包含 20 个实体类型的 schema生成全部操作后数据层代码体积会明显膨胀。这不一定是问题因为鸿蒙的 Flutter 引擎在 AOT 编译时会对这些 Dart 类做很好的优化真正需要关注的是首次构建时间的增长。我实测在中等规模工程下第一次完整 build 时长比纯手写模型工程的构建多出约 20%但后续增量构建因为有.dart_tool的缓存加持增量成本几乎可以忽略。另一个值得观察的点是启动阶段FerryClient 的初始化是纯内存操作耗时极短只要不在客户端构造时同步加载大量本地数据瓶颈基本不在数据层。真正的冷启动成本在远端请求首帧返回时间上建议把 client 初始化做成单例并在main()里提前做避免页面首帧渲染时才触发初始化导致白屏。5. 常见问题与排查技巧5.1 问题现象速查表把适配过程中遇到的高频问题整理成一张表方便团队按图索骥问题现象可能原因解决方式build_runner 报 analyzer 版本冲突鸿蒙 Dart SDK 版本过老依赖解析出的 analyzer 过新锁定 build_runner/analyzer 版本必要时 dependency_overrides生成时提示找不到 schema.graphqlschema 文件未放在 lib 目录或文件名不一致确认文件位置和命名放在 lib 下任意目录即可生成的代码 import 报错gql相关包找不到pub get 未重跑或 lock 文件被误删删除.dart_tool后重新flutter pub getHTTP 请求成功但 response 永远无 dataDio 默认把 500 状态当异常ferry 拿不到响应体配置validateStatus为状态码小于 500应用启动时 hive 初始化报目录错误沙盒路径未正确指定初始化 hive 时指定完整绝对路径WebSocket 订阅连接后收不到推送鸿蒙 WebSocket 通道与web_socket_channel兼容问题短期内用轮询替代或封装原生 WebSocket Link首次 build 时间过长生成器全量生成文件数量多按模块拆分 graphql 文件合理使用 fragment 复用Flutter module 依赖变更后鸿蒙工程未更新构建缓存未清理清理build目录和.dart_tool后重新编译5.2 几条真正的独家避坑经验走过一遍完整适配后有几点属于常规文档里不会写、但实际能省大量排查时间的心得第一先把生成链路在命令行里单独跑通再进 IDE。鸿蒙工程集成 Flutter module 后IDE 里的构建链路更长报错信息容易被上层包装吞掉。我都是从终端直接执行dart run build_runner build把输出日志完整拉出来确认生成没有问题时再进 IDE 做集成验证。这样可以把“生成器问题”和“鸿蒙集成问题”这两类故障干净地切开。第二统一 schema 来源。项目里如果有多个后端开发并行改 schema一定要把schema.graphql的更新纳入版本管理并且让后端每次变更都同步提交最新 schema 文件。一次 schema 和代码不同步生成的类型全是错位页面上的表现又通常只是某个字段为空排查起来极其费劲。第三把 FerryClient 做成真正意义上的单例。不要在每次打开页面时重建 client。ferry 的 Cache 一旦重建之前累积的查询缓存、fragment 缓存全部失效网络请求量会翻倍。我在鸿蒙工程里采用的是顶层变量延迟初始化配合late final语法保证全局唯一。第四审慎引入持久化缓存。鸿蒙的沙盒路径和 Android 不完全一致hive 这类存储库在鸿蒙上虽然能跑但文件目录一旦写死就容易踩坑。优先保证内存缓存链路稳定再决定是否上离线缓存如果要用先在鸿蒙模拟器上完整验证一遍冷启动后的恢复流程。第五版本升级要全链路验证。ferry_generator 升级一个大版本生成的代码结构可能有变化调用点都要跟着调整。建议升级时先在独立分支跑一遍全量生成再对比生成文件的 diff确认没有破坏性变更后再合入主分支。最后分享一点个人体会。给 ferry_generator 做鸿蒙化适配本质上不是把一个包从 A 平台搬到 B 平台而是要重新审视一条完整的数据链路在目标平台上的每一环是否可靠。生成器本身是纯 Dart 工具迁移成本最低但网络层、缓存层、构建链路这些“周边设施”才是决定适配成败的关键。我在这次实践中最大的收获是先画清楚依赖分层把可变部分网络栈和不可变部分生成逻辑隔离好遇到问题就能快速定位是环境、依赖还是代码的问题。鸿蒙生态还在快速演进纯 Dart 包的适配难度会逐步降低但架构上预留可替换网络层、统一 Schema 管理这些基本功在任何平台上都不过时。
返回列表