最近我们团队在把一套老 Flutter 工程往鸿蒙环境迁移,最棘手的不是页面本身,而是工程里的基建层——大家一直用的 fast_base 脚手架在鸿蒙 Flutter 引擎上没法直接工作,所有页面、仓储、依赖注入关系全都跟着卡壳。
先给不熟悉的朋友解释下:fast_base 不是 UI 组件库,它是一套“极速搭架子”的基础架构库。它把 Flutter 项目从零到一的过程中那堆重复劳动收敛了,比如应用初始化、页面控制器生命周期、Repository 的统一读写通道、业务模型注入的容器,以及一套响应式的状态通知机制。说白了,它把“建控制器、连仓储、绑模型、管刷新”这串样板活全部吞掉,让业务开发只关心数据本身。
这篇文章我把 fast_base 鸿蒙化适配的完整路线整理出来,包括整体思路、改造步骤、构建配置、踩坑记录,给正在做鸿蒙 Flutter 适配、或者打算拿 fast_base 做新工程基座的开发者一个可复用的参考。内容不需要你读过多背景知识,只要写过 Flutter、知道 Repository 是什么概念,就能跟上节奏。
1. 先搞清楚 fast_base 为什么值得迁移
1.1 它解决的三个核心痛点
先说结论:fast_base 之所以在团队里被依赖得这么深,是因为它在三个点上做得非常到位,而这三个点恰恰是 Flutter 工程最容易滋生重复代码的地方。
第一个痛点是项目起步慢。搭一个新页面,你得先建 StatefulWidget、建 Controller、初始化 Repository、手动管理 dispose、处理页面数据回传……一套流程走下来半天没了,而且每个页面长得都差不多。fast_base 把 FastApp、FastPage、FastController 这套基类扔给你,页面继承之后,控制器生命周期、订阅释放、首次数据加载全部自动处理。
第二个痛点是 Repository 与数据源的耦合。很多团队的 Repository 里直接堆网络请求、缓存读写、日志上报,换一个数据源就要重写一遍。fast_base 的 BaseRepository 把“数据从哪来”抽象成可替换的策略,内存缓存、持久化缓存、远端请求可以按优先级串起来,业务侧拿到的永远是模型对象,不用关心数据是哪里来的。
第三个痛点是业务模型注入的灵活性。同一个 BaseRepository 可能被多个业务模型复用,fast_base 通过泛型 + 注入容器把模型实例传给仓储层,一个仓储可以处理多种模型类型,不需要为每个模型写一套 CRUD。这对于中后台类应用、表单流转场景特别实用。
1.2 模块划分与数据流
fast_base 的模块边界很清晰,我从适配角度把关键模块拆开看:
| 模块 | 职责 | 鸿蒙化适配的重点 |
|---|---|---|
| FastApp | 应用入口、全局配置、路由表初始化 | 检查初始化时机与生命周期绑定 |
| FastPage | 页面基类,自动装配控制器与订阅 | 检查 mixin 语法与 dispose 触发 |
| FastController | 业务控制器,承载页面状态与响应式刷新 | 检查 Listenable 与 ValueNotifier 使用 |
| BaseRepository | 统一数据读写,缓存策略编排 | 检查泛型、异步与存储实现 |
| FastContainer | 轻量依赖注入容器,支持模型注入 | 检查类型注册与工厂方法 |
| FastModel | 业务模型基类,内置 JSON 序列化骨架 | 检查动态类型与序列化实现 |
数据流大概是这样的:FastPage 创建时自动装配 FastController,Controller 持有 BaseRepository 和业务模型实例;页面里的数据变化通过 ValueNotifier 或自定义 Listenable 广播出去,UI 侧通过 ValueListenableBuilder 订阅自动刷新;仓储层收到控制器请求后,先查缓存、再打远端,最后把结果写回模型对象。整条链路是单向的,这对鸿蒙化适配很友好——只要把链路里依赖平台能力的地方替换掉,其他部分都能保留。
1.3 与其它框架的取舍
有人会问:迁移这么费劲,为什么不直接换成 Riverpod 或者 GetX,非要咬着 fast_base 不放?
我当时的判断是:换框架的隐性成本远高于适配成本。GetX 的问题是耦合重,它把路由、状态、依赖注入全揉在一个包里,换到鸿蒙环境后你根本分不清是哪个模块在报错;Riverpod 的状态管理思路很好,但它对“模型注入到仓储”这种偏后端思维的场景支持不够直接,得手写大量 Provider。
fast_base 的“轻底座”意味着它的绝大部分代码是纯 Dart,不依赖具体平台的原生能力。适配的时候核心思路是:把平台相关的通道替换掉,把纯 Dart 的架构逻辑保留。这种架构上的内聚度,决定了它的迁移成本天然就比大而全的框架低。
2. 鸿蒙化适配的前置分析:先别急着写代码
2.1 鸿蒙 Flutter 运行时的差异点
任何鸿蒙 Flutter 适配工作的第一步,都是先理解运行环境差异。鸿蒙上的 Flutter 应用跑在一套独立的引擎分支上,底层渲染、事件分发、生命周期管理与标准 Flutter 引擎并不完全一致,但好在 Dart 语言层和 Widget 层的大部分 API 都兼容。
我们实际碰到的差异主要集中在三块。第一块是原生插件生态:很多 Flutter 插件(比如 shared_preferences、path_provider)在鸿蒙上没有现成的原生实现,MethodChannel 的 native 端没人响应,代码跑起来就静默失败。第二块是编译约束:鸿蒙侧的构建工具链对代码的静态检查更严格,Dart 的动态类型推断、隐式转换在一些场景会被拦截。第三块是平台标识:代码里如果用了Platform.isAndroid这类分支判断,在鸿蒙环境里两个都不会命中,逻辑会掉到默认分支。
这三块差异决定了适配计划不能是“把代码复制过去编译试试”,而要先做一轮系统的依赖树扫描和静态兼容性检查。
2.2 依赖树扫描:先圈出需要替换的插件
适配前我做的第一件事,是在工程根目录跑flutter pub deps --style=compact或直接在 pubspec.lock 里扫一遍依赖树,把所有传递依赖列出来,然后逐项标记“是否涉及平台原生能力”。
重点标记的对象包括:
- 直接依赖原生代码的包,比如 shared_preferences、path_provider、sqflite
- 内部通过 MethodChannel 与原生通信的包,比如各种统计 SDK、支付 SDK
- 依赖 dart:io 文件读写、网络套接字的包
- 依赖
package_info、device_info这类系统信息查询的包
拿到清单后,按“可替换”“需适配”“必须重构”三类归类。可替换的比如 shared_preferences,鸿蒙环境有对应的 KV 存储方案,写一层适配接口就行;需适配的比如网络库 dio,它在鸿蒙 Flutter 引擎上可以跑纯 Dart 模式,但如果有证书校验、代理设置之类的原生依赖要单独处理;必须重构的那部分,就是跟业务强绑定、又依赖了非通用插件能力的模块,这类要重点拆解。
2.3 静态兼容性检查清单
扫描完依赖树,还要过一遍代码层面的兼容性检查。我整理了一个 Checklist,你在开始改代码之前也照着一项项过:
- 代码里是否用到
dart:io的专属能力,比如 File、Socket、Process - 是否出现
Platform.isAndroid或Platform.isIOS的分支逻辑 - 是否有直接 import 第三方插件的 Dart 实现文件并调用内部方法
- 是否有
Isolate.spawn或compute的多线程场景,鸿蒙引擎对 isolate 的支持范围要确认 - 是否用了
dynamic关键字做非受控类型转换 - 是否依赖
package:flutter/foundation.dart里与默认平台相关的常量 - 是否有资源文件路径硬编码,鸿蒙的字体、图片资源目录结构与标准 Flutter 不完全一样
这一遍检查非常值。我们当时在两个模块里发现了Platform.isAndroid分支,直接导致功能在鸿蒙上走了完全错误的路,如果靠运行时去试错,排查周期会拉长好几倍。
3. 核心改造:Repository 封装与业务模型注入的落地细节
3.1 泛型 Repository 的语法兼容处理
fast_base 的 BaseRepository 是用泛型实现的,这在标准 Flutter 上没有任何问题,但鸿蒙侧的编译器有时候对泛型边界的检查比较严格。我们的处理方式是给泛型加上明确的类型边界,避免使用裸的T或dynamic。
改造前的典型写法:
class BaseRepository<T> { final T? Function(dynamic data) _parser; BaseRepository(this._parser); }改造后变成:
class BaseRepository<T extends FastModel> { final T? Function(Map<String, dynamic> data) _parser; BaseRepository(this._parser); }这么改的好处有两层:一是编译器能确认 T 一定是 FastModel 的子类,可以安全调用 toJson、fromJson;二是鸿蒙侧的静态检查不再抱怨“无法确认类型是否存在某个方法”。如果你们的模型压根不继承 FastModel,那就给 T 加一个接口约束,原理是一样的。
3.2 响应式状态机制的通道改造
fast_base 的“响应式”核心是 ValueNotifier 和自定义 Listenable 的组合。页面通过监听这些对象来刷新 UI,Controller 数据变化后直接给 notifier 赋值。
在鸿蒙 Flutter 引擎上,ValueNotifier 本身是可用的,但踩坑点在于事件广播时机。标准 Flutter 里notifyListeners会触发所有监听者的build,鸿蒙引擎在某些低版本上有过框架层调度差异,偶发出现“数据变了但页面没刷新”的情况。我们的做法是统一封装一套FastObservable<T>:
class FastObservable<T extends FastModel> extends ValueNotifier<T?> { FastObservable([T? initial]) : super(initial); void emit(T? data) { if (value == data) return; // 避免无效通知 value = data; } void update(void Function(T? current) mutator) { final current = value; mutator(current); notifyListeners(); } }这里有个实操细节:emit方法里加了一个“值没有变化就不通知”的守卫。一开始我们没有加,结果鸿蒙上某些构建周期的微小差异导致 UI 反复重建,页面滚动时明显卡顿。加了这个守卫之后,无效刷新大幅减少,性能整体上升了一个台阶。
3.3 本地存储从 shared_preferences 到轻量 KV
fast_base 内部对本地存储做了一层抽象,默认实现是 shared_preferences。鸿蒙化之后,这层必须换成鸿蒙侧的轻量 KV 存储。
我采用的方案是在 Repository 接口不变的前提下,写一个HmKVStorage实现。关键是不要让业务代码感知到底层换了存储,也就是仓储对外暴露的read / write / delete方法签名完全一样,只是内部实现从共享偏好切换成鸿蒙的 KV 能力。
class HmKVStorage implements FastKV { @override Future<String?> read(String key) { // 调用鸿蒙侧持久化能力获取 key 对应的值 } @override Future<void> write(String key, String? value) { // 写入鸿蒙侧 KV 存储 } }另一个要注意的是缓存 Key 的命名空间。老工程在 Android 上用的是应用包名作为前缀,鸿蒙环境下这个前缀不再唯一,我们统一改成了「模块名 + 业务 Key」的格式,顺手解决了一个历史遗留的缓存串数据问题。
3.4 业务模型注入的容器调整
fast_base 的业务模型注入依赖 FastContainer。模型通过FastContainer.put<T>(T model)注册,Repository 通过FastContainer.get<T>()取用。
鸿蒙化过程中,我们发现容器本身不用大改,但有两个点必须注意。
第一是泛型类型擦除问题:如果注册和取用的时候泛型写得不一致,运行时会有type 'xxx' is not a subtype of type 'yyy' in type cast的报错。我们在容器内部改用Type对象做精确匹配,注册的时候自动记录当前运行时类型,取用的时候按运行时类型查找,绕开了擦除问题。
第二是注入时机问题。老代码里有不少FastContainer.get<T>()是在模块初始化之前被调用的,在标准 Flutter 上由于初始化顺序相对宽松,偶尔能跑,但鸿蒙的生命周期检查和启动顺序更严,直接抛空指针。我们的处理是引入 factory 方法注入,把“注册模型实例”改成“注册模型工厂”:
FastContainer.register<T>(() => T());这样模型真正被需要的时候才创建,既解决了初始化顺序问题,也减少了启动时的内存占用。
3.5 序列化与动态类型处理
FastModel 基类提供了fromJson和toJson的默认实现,业务模型只需要声明字段类型。适配鸿蒙时,序列化这块的坑主要在嵌套模型的动态解析上。
fast_base 的默认做法是字段上标@JsonKey,然后靠反射或代码生成工具去跑。鸿蒙 Flutter 引擎对标*准的 dart:mirrors 反射支持有限,所以不能依赖反射做运行时类型解析。我们全部切到代码生成方案,也就是用 json_serializable 这类 build_runner 工具,在编译期就把序列化代码生成好,运行时不依赖反射。
这块的经验是:模型越多,越早切代码生成越省事。我们起初想着只修几个报错就行,后来发现隔三差五就有_InternalLinkedHashMap转不了模型的问题,最后还是一把梭切到代码生成,再没出过序列化相关的幺蛾子。
4. 构建与验证:把鸿蒙包真正跑起来
4.1 构建工具链与依赖版本锁定
鸿蒙 Flutter 工程的构建与标准 Flutter 不一样,工程级配置会更复杂。我们踩下来的实操路径是:先保证 Flutter 侧依赖版本锁定,再处理鸿蒙构建配置。
依赖版本锁定听起来简单,做起来容易漏。fast_base 本身传递了很多间接依赖,比如dio、connectivity_plus、intl,每个都可能在鸿蒙引擎上有版本兼容差异;不确定哪个版本可用时,不要一次性升级一堆,而是每次只动一个依赖,构建一遍验证一遍,确认没问题再动下一个。我们当时一次升级了三个依赖,结果报错之后根本定位不到是哪个引起的,只能逐个回退排查,浪费了大半天。
构建侧的配置重点放在权限声明和资源目录。鸿蒙应用需要声明网络权限、存储权限等,权限声明文件里如果漏了,运行时会一直报“无权限”,但错误信息又不会直接告诉你缺哪个权限。建议在适配初期就把权限清单整理好,把用到的系统能力一次性声明齐全。
4.2 产物验证与初始化耗时对比
适配完成后不能只看“能跑”,要做一次完整的产物验证和数据对比。
我们侧的重点是三项:冷启动耗时、首个页面打开耗时、包体增量。
| 指标 | 适配前(Android) | 适配后(鸿蒙) | 说明 |
|---|---|---|---|
| 冷启动到 Flutter 首帧 | 正常范围 | 正常范围,稍高 | 鸿蒙引擎启动初始化略重 |
| 首个业务页面打开 | 快 | 快 | 仓储缓存联动正常 |
| 安装包体积增量 | 基线 | 增加约 15% | 主要来自引擎与原生适配层 |
首帧之后,进入业务页面的速度差异几乎感知不到。FastObservable 的重建守卫在这次对比里也立功了,页面快速切换时没有出现状态滞留和 UI 闪烁。
4.3 多设备兼容性验证
鸿蒙的设备形态跨度比较大,从手机到平板再到车机,屏幕尺寸和交互方式差别很大。我们做了一轮基础兼容验证,重点不是 UI 适配,而是 fast_base 里跟页面生命周期和 Repository 刷新相关的行为。
结论是:fast_base 的页面基类和仓储逻辑与屏幕尺寸无关,纯 Dart 层的表现一致,但有两处需要预留扩展点。一是平板宽度较大时,FastPage 默认的布局约束需要重写;二是车机环境没有触摸事件,Controller 里如果绑定了手势回调,会直接失效。这两处建议在适配阶段的基类里预留 override 口子,不要等业务侧再绕过框架去 hack。
5. 常见问题速查与排查心得
5.1 编译期类型转换报错
这个是我们遇到频率最高的问题。type 'xxx' is not a subtype of type 'yyy' in type cast一出现,基本可以断定是序列化或注入容器的类型不匹配。
排查套路:先看报错堆栈里是哪个类在强转,然后检查 FastContainer 注册的泛型类型和取用的泛型类型是否一致;再看 JSON 数据是不是符合模型字段声明,尤其是嵌套对象和数组类型。顺手在模型类里给每个字段加好类型断言,错误信息会清晰很多。
5.2 运行时 MethodChannel 无响应
表现为调用某个插件方法后回调永远不触发,不报错也不超时。原因是鸿蒙侧没有实现对应原生模块。
检查方法是直接搜索MethodChannel和EventChannel的使用点,看有没有插件在 pubspec 里声明了但鸿蒙侧没有对应能力。我们的应对是在调试模式下包一层channel.setMockMethodCallHandler先确认调用链走通,然后逐个替换成鸿蒙原生的实现。
5.3 资源路径找不到导致的图片白屏
鸿蒙上图片和字体的资源目录结构与标准 Flutter 不完全相同,最直接的排查方式是打开构建产物目录,检查资源文件的实际路径与代码里引用的路径是否一致。
我们遇到过一次字体加载失败,界面上的自定义图标全部变空白,排查到后面发现是字体文件在构建时被重新命名了。这个问题的解法是显式指定字体文件名,而不是依赖默认资源映射。
5.4 我的排查套路总结
三句话:先复现再定位,能关的模块就先关掉;先扫调用链再查数据;先怀疑插件再怀疑框架。fast_base 的大部分问题本质上不是框架本身的问题,而是它依赖的某个下游能力在鸿蒙上没有对齐,顺着调用链找下去,一定能在某一层找到原生的缺口。
我个人在实际操作中最深的体会是:适配工作要“先架构、后编码”。把依赖树和静态兼容性清单梳理清楚,明确定位到每一层需要改什么、能保留什么之后,真正动手改的时间反而不长。fast_base 的抽象设计在鸿蒙化过程中帮了大忙,它把平台相关的代码收得非常干净,这一点是它能顺利迁移的关键前提。
最后再分享一个实用技巧:给 fast_base 在鸿蒙侧的适配单独开一个分支,保持主工程不动。适配过程中所有临时改动都集中在分支里,遇到问题可以随时切回主分支对比行为差异。我们全靠这个分支降低了不少排查难度,建议你迁移任何基础库的时候都这么干。