ARTICLE DETAIL

资讯详情

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

Flutter鸿蒙化适配:用kiss_repository构建跨平台数据访问层

Flutter鸿蒙化适配:用kiss_repository构建跨平台数据访问层 去年接到鸿蒙化任务那段日子我几乎每天都在和各种 Flutter 三方库的兼容性问题较劲。业务代码跑起来之后最先崩掉的就是数据访问层原本依赖 Android 平台通道才能拿到的本地缓存、设备参数和信息持久化结果在 HarmonyOS NEXT 的 Flutter 运行环境里全部失效。排查了两天后我确认根子不在业务代码而在没有一条跨平台一致的数据访问通道。于是我把目光转向 kiss_repository 这个一贯主张极简主义的仓库模式方案做了一轮完整的鸿蒙化适配最终在项目里落地了一套干净、可替换、可测试的鸿蒙数据访问层。这篇把适配过程、关键判断和踩过的坑都写出来给正在折腾 Flutter 鸿蒙化改造的人一条可以直接抄的路线。1. 先拆解 kiss_repository 的设计内核极简数据层凭什么抗住鸿蒙化改造1.1 Repository 模式最容易长成大泥球kiss_repository 偏不做 Flutter 的人对 Repository 模式都不陌生把数据从哪来、怎么存、怎么发给页面这一整套逻辑从业务代码里抽出来收敛到一个仓库层。这个思路本身没问题但我在大量项目里见过同一个灾难现场——仓库层把 Dio、数据库连接、SharedPreferences、状态管理全部揉在一起一个构造器拉出来七八个依赖页面里还要自己做缓存判断代码之间彼此勾连改一个缓存逻辑要牵扯十几个类。headache 的根源是模型混乱大家把仓库当成了万能收纳箱什么都往里塞。kiss_repository 这个库的思路正好相反它从名字就表明了立场——Keep It Simple, Stupid。它不搞代码生成不绑定任何状态管理框架不做网络请求的二次封装核心只有两个抽象概念数据源DataSource和仓库Repository再加一个可选的缓存策略。典型用法是一个泛型仓库接口搭配两三个数据源实现十几行代码就把网络优先、本地兜底的逻辑立住了。这种薄封装的方式在鸿蒙化场景里尤其占便宜。因为鸿蒙的 Flutter 运行时和 Android/iOS 不完全一致任何多余的框架依赖都可能成为适配的拦路虎而 kiss_repository 几乎没有多余的东西可以出问题。1.2 和重型方案对比轻量不是缺点是边界感很多团队喜欢自研 Repository 层然后和状态管理框架深度绑定比如配 Riverpod、配 GetX或者上更重的数据层框架。我用一个表格把这几种路线的差异摆出来方便判断什么场景该选谁维度kiss_repository自研 Repository Riverpod自研 Repository GetX核心概念数量2 个抽象类至少 5 个概念Provider、State、Repository、DataSource 等至少 5 个概念Controller、Repository、Service 等代码生成无无可选平台通道依赖无取决于业务代码取决于业务代码鸿蒙化适配成本低几乎纯 Dart 可用中看自研代码是否夹带平台能力中高GetX 的路由/生命周期钩子要单独确认单元测试难度低直接 mock 数据源中需要组装 Provider 树中Controller 生命周期要处理学习成本低中中这里我想多说一句很多团队选重型方案不是因为他们需要那么重的抽象而是因为自研仓库写着写着就膨胀了。Riverpod 和 GetX 本身没问题但如果你要的只是把数据来源收口那 kiss_repository 这种极简库反而更稳——抽象越少暴露给平台差异的缝隙就越小到了鸿蒙这种新运行时上能出问题的面就窄。1.3 纯 Dart 属性是鸿蒙化的第一前提HarmonyOS NEXT 的 Flutter 支持来自社区维护的 Flutter 引擎分支它把 Dart 层兼容做得很不错但 Platform Channel、EventChannel、PlatformView 这些桥接层的能力和 Android/iOS 相比仍有差距部分插件根本没有鸿蒙实现。一个纯 Dart 库在鸿蒙上几乎可以原样跑一旦夹带原生插件就得逐个确认有没有鸿蒙版本或者自己补一套。kiss_repository 的源码全是纯 Dart 类不碰 dart:io不碰 MethodChannel不依赖任何平台插件这也是它能顺利完成鸿蒙化适配的根本前提。但注意这不代表整个项目就万事大吉了——你的业务工程里很可能为了存缓存、取路径、拿设备信息而引入了别的插件。所以真正要做的是底数排查把整个依赖关系树的平台依赖情况摸清楚这也是下一章的重点。2. 适配前的底数排查把三方库的纯 Dart 属性验明正身2.1 用 flutter pub deps 揪出依赖树里的隐形平台插件很多人拿到鸿蒙化任务的第一反应是直接打开编辑器改代码这其实是错的。适配第一步应该是静态排查先看清 kiss_repository 和整个业务工程的依赖树里到底有没有夹带平台能力。我最常用的命令就一条flutter pub deps --stylecompact注意这里加--stylecompact它会把依赖树的缩进拉平输出更适合横向扫描你能快速定位到path_provider、shared_preferences、url_launcher、video_player、connectivity_plus这类常见插件。这些插件如果出现了就说明工程存在平台通道依赖需要进一步确认鸿蒙侧有没有对应实现。我当时排查 kiss_repository 的依赖树时输出非常干净只有几个纯 Dart 包。这给了我先跑通再细调的底气。如果你们的依赖树里出现了上面的插件也别慌后面我会给出常见插件的鸿蒙替换对照表。2.2 源码审计盯死 dart:io、dart:ffi 和 MethodChannel依赖树干净不代表源码里没有暗雷。有些库会在特定场景下偷偷 import 平台相关的能力。我的做法是直接进到三方库的lib目录用 grep 扫几个高危关键词grep -r dart:io lib/ grep -r dart:ffi lib/ grep -r MethodChannel\|EventChannel\|BasicMessageChannel lib/ grep -r Platform\.is lib/为什么重点扫这几个dart:io一般意味着文件系统访问鸿蒙的沙箱路径和 Android 不一样盲用一定会出问题dart:ffi意味着调用原生 so 库这类库基本断送了鸿蒙化的低成本路径Platform.isAndroid这种分支判断在海思鸿蒙真机上会走到哪条分支取决于 Flutter 引擎怎么上报很多时候会识别成 Android但行为又和 Android 不一致非常容易踩坑。如果 grep 出来只有测试文件里有这些引用那问题不大说明库的主干逻辑是干净的。真正要警惕的是主干源码里出现MethodChannel那意味着库需要原生侧配合适配工作量直接上一个台阶。2.3 用 flutter analyze 和 flutter test 铺一遍基线排查完源码我把 Flutter SDK 切到支持鸿蒙的 flutter_flutter 分支然后跑两条命令flutter pub get flutter analyze flutter testflutter analyze的作用是检查 API 兼容性Dart SDK 版本不对或者引用了已废弃 API 都会在这里暴露。flutter test是跑纯 Dart 层的单元测试这一步非常关键——kiss_repository 的测试如果全绿说明核心逻辑在 Dart VM 上是自洽的不依赖原生环境。有一点必须说清楚flutter test跑在纯 Dart VM 上不能代表鸿蒙真机一定没问题它只能过滤掉绝大多数静态和逻辑层面的错误。真正要过鸿蒙这一关还得靠后续的真机验证。但先把这两条命令跑绿能帮你在后面的排障里少掉一半头发。3. 鸿蒙工程落地改造从 pubspec 到 hvigor 构建链路的完整操作3.1 给 Flutter 工程补上 ohos 入口目录标准的 Flutter 工程默认只有 android、ios 等目录要跑在鸿蒙上需要在工程根目录创建ohos目录。如果你安装的是社区版的 Flutter 引擎通常配套的 DevEco Studio 插件可以帮你从向导生成入口也可以手工创建目录结构大致是这样ohos/ ├── AppScope/ │ ├── app.json5 │ └── resources/ ├── entry/ │ ├── build-profile.json5 │ ├── hvigorfile.ts │ ├── oh-package.json5 │ └── src/main/ │ ├── module.json5 │ ├── ets/ │ └── resources/ ├── build-profile.json5 ├── hvigorfile.ts └── oh-package.json5这个目录的作用类似 Android 的 gradle 工程目录module.json5里要声明应用权限、页面入口、ability 配置。如果你的 Flutter 应用用到了摄像头、网络状态、存储权限需要在这里提前把权限声明补上否则运行期会直接闪退或静默失败。3.2 pubspec.yaml 的调整策略与存储插件的鸿蒙替换kiss_repository 本身不需要改 pubspec但业务工程里那些存储和路径插件必须处理。我整理了一份常用插件的鸿蒙替换对照表都是实践中确认有效的方案原插件鸿蒙化替代/方案说明shared_preferencesshared_preferences_harmony 或自研轻量存储数据仍存本地但路径和格式与 Android 不同path_providerpath_provider_harmony文档目录、缓存目录映射到鸿蒙沙箱connectivity_plus鸿蒙实现或纯 Dart 探测 fallback断网判断逻辑必须重新验证dio无需替换纯 Dart 网络库底层走 Dart 的 HttpClientflutter_secure_storage鸿蒙 KeyStore 封装注意没有现成方案时需自己封装package_info_pluspackage_info_plus_harmony 或手动读取版本信息获取方式不同对应到 pubspec.yaml 里的改法大致是这样dependencies: flutter: sdk: flutter kiss_repository: ^1.0.0 dio: ^5.0.0 shared_preferences_harmony: ^0.0.5 path_provider_harmony: ^0.0.3这里有个细节鸿蒙版插件通常以_harmony后缀命名它们的 API 不一定和原版完全一致比如shared_preferences_harmony的异步接口可能略有差异。替换之后凡是调用了这些插件的地方都要重新编译过一遍逐个修正。3.3 构建链路hvigorw 和 DevEco 的版本对齐问题配置好以后最直接的构建方式是用命令行hvigorw assembleHap --mode module -p productdefault这条命令会把整个鸿蒙工程编译成 HAP 安装包。但命令行适合验证构建日常调还是建议用 DevEco Studio启动和断点调试都方便。这个环节最容易踩的坑是版本对齐Flutter 引擎分支版本、鸿蒙 SDK 版本、DevEco Studio 版本三者必须匹配。我遇到过一次 Flutter 引擎太旧、鸿蒙 API 12 的 SDK 太新的情况导致 module.json5 里的权限声明被构建工具裁剪应用启动时直接闪退日志里完全看不出来是权限问题折腾了很久才定位到是版本错位。建议你在开始之前先去对应社区仓库确认Flutter 引擎版本 vs 鸿蒙 SDK 版本的兼容矩阵把环境一次配齐。版本对齐这件事没有任何技巧可言纯粹是先确认再动手。4. 数据访问层骨架一套可以直接落地的鸿蒙数据层设计方案4.1 用 DataSource 和 Repository 两个抽象撑起整层逻辑kiss_repository 的核心理念落到代码上其实特别朴素定义数据源抽象让仓库去组合它们。我这里给你一套我最终在鸿蒙项目里使用的写法你可以直接抄/// 数据源抽象只干一件事取数据 abstract interface class DataSourceT { FutureT fetch(); } /// 本地数据源包装 SharedPreferences、文件或内存 class LocalDataSourceT implements DataSourceT { LocalDataSource(this._read); final FutureT Function() _read; override FutureT fetch() _read(); } /// 远端数据源包装 Dio 请求、WebSocket 等 class RemoteDataSourceT implements DataSourceT { RemoteDataSource(this._fetch); final FutureT Function() _fetch; override FutureT fetch() _fetch(); } /// 仓库组合数据源对外只暴露干净的数据入口 class RepositoryT { Repository({ required this.remote, required this.local, this.policy const CacheFirst(), }); final RemoteDataSourceT remote; final LocalDataSourceT local; final CachePolicyT policy; FutureT get() policy.execute(remote, local); }这套代码的核心价值在于页面只认识Repository.get()它不关心数据是来自网络还是缓存。两个数据源互相独立测试时你可以给RemoteDataSource注入一个抛错的方法验证降级逻辑完全不需要 mock 任何鸿蒙平台能力。4.2 缓存策略TTL 过期与内存缓存不依赖任何平台通道有了数据源和仓库接下来是缓存策略。我把CacheFirst这种最常见的策略实现出来它代表的是先读缓存过期了再回源abstract class CachePolicyT { FutureT execute(DataSourceT remote, DataSourceT local); } class CacheFirstT implements CachePolicyT { CacheFirst({this.ttl const Duration(minutes: 5)}); final Duration ttl; override FutureT execute(DataSourceT remote, DataSourceT local) async { final cached await local.fetch(); if (cached ! null _isFresh(cached)) { return cached; } final data await remote.fetch(); await _writeBack(data); return data; } bool _isFresh(T data) { // 根据数据携带的时间戳判断是否过期 // 这里的时间获取用 DateTime.now()不依赖任何平台通道 return true; } }注意我特别标注了时间点的选择DateTime.now()在 Dart 层是通用的天然跨平台所以 TTL 逻辑放在纯 Dart 层完全可行。反之如果你在鸿蒙数据层里写死了path_provider或者直接操作本地文件路径就会和平台强绑定违背了极简数据层的初衷。缓存文件存哪里交给上层的path_provider_harmony决定缓存策略本身不关心路径。4.3 错误处理与降级在仓库层消化平台差异鸿蒙和 Android 的错误体系不一样Android 上的一些平台异常到了鸿蒙可能变成别的错误类型。我的做法是让仓库层统一处理错误对外返回一个结果包装类型而不是抛出五花八门的异常sealed class AppResultT { const AppResult(); } class SuccessT extends AppResultT { const Success(this.data); final T data; } class FailureT extends AppResultT { const Failure(this.error, {this.stackTrace}); final Object error; final StackTrace? stackTrace; } FutureAppResultT safeFetchT(RepositoryT repo) async { try { final data await repo.get(); return Success(data); } catch (e, st) { return Failure(e, stackTrace: st); } }这样页面层只处理Success和Failure两种状态任何平台差异都在仓库层被消化掉了。实际运行中鸿蒙的断网错误、沙箱权限错误、未实现插件错误都会在这个safeFetch里被统一接住。你可以在Failure里加日志上报把错误堆栈集中收集排障效率能提升一大截。5. 排障实录真正跑起来才会撞上的四个典型问题5.1 构建期插件产物找不到ohos 依赖机制和 Gradle 不是一回事第一次用 hvigorw 构建时我遇到了一个看着很奇怪的问题日志里报某个插件产物找不到具体是libflutter.so相关的加载失败。一开始我以为是插件没装干净反复清理重建问题依旧。排查链路走下来才发现鸿蒙插件的依赖机制和 Android 的 Gradle 体系完全不同——Android 插件通过 gradle 仓库拉取鸿蒙插件则是通过oh-package.json5声明依赖然后由 hvigor 把 so 库打进 HAP。正确的排查顺序是先看ohos/entry/oh-package.json5里有没有声明对应的鸿蒙插件依赖再看entry的 dependencies 作用域是否正确最后才考虑清理构建产物。我当时就是漏了第一步直接跳到清理白折腾了一个多小时。5.2 运行期EventChannel 初始化时序比 Android 严格得多第二次踩坑是在项目里接一个定位插件时偶发报错说 channel 未注册。在 Android 上这个插件从来没出过问题到了鸿蒙上大概每五次启动有一次失败。我查了日志发现 ArKTS 侧的插件注册时机比 Android 更严格Android 允许在引擎初始化过程中边初始化边注册鸿蒙这边则要求 Flutter 引擎初始化完成之后才能建立 EventChannel 通道。解决方式是把插件的初始化逻辑从应用启动阶段挪到第一次实际访问数据源之前用一个懒加载的单例去管理连接。这个改动很小但如果你不做偶发失败会让你误以为是数据层网络问题排查方向整个跑偏。5.3 渲染层PlatformView 导致的内存抖动别扯到数据层头上我们的项目里有一张地图页面用的是 WebView 实现。鸿蒙上 Flutter 的 PlatformView 支持还不算成熟切后台再切回来时内存出现明显抖动甚至偶发黑屏。最初我怀疑是数据层某个缓存对象泄漏了抓了一遍堆内存没发现 Flutter 侧异常进一步用 DevEco 的性能工具观察 ArKTS 侧才发现问题出在原生纹理共享上。这个问题的结论是数据层的设计和 PlatformView 的内存抖动没有直接关系但在实测阶段要警惕它干扰你的判断。如果你也遇到莫名其妙的卡顿先排查页面里有没有 PlatformView不要盲目改数据层。5.4 数据层本地缓存格式不兼容老用户升级后登录态丢失这是我这次适配里最掉头发的问题。用户从 Android 版升级到鸿蒙版后发现登录态和草稿全丢了。排查下来shared_preferences和shared_preferences_harmony虽然都叫本地键值存储但存储路径完全隔离鸿蒙版读不到 Android 版留下的文件。这时候不要硬把两个格式打通太脆。我的做法是在鸿蒙版首次启动时加一个迁移标记检测到这是首次启动就把 Android 沙箱里可导出的数据一次性搬运到鸿蒙沙箱搬运完再置标记位。这个逻辑正好可以放进 kiss_repository 的缓存层里用一个专门的MigrationSource去处理。数据迁移这种事越早设计越好放到项目上线后再补就难了。6. 验收清单与我的实际体会6.1 一套可复用的鸿蒙数据层验收路径适配做完不是能跑就行我给自己定了一套验收路径你可以直接照用flutter analyze零错误这个是底线flutter test全绿覆盖率至少保住核心缓存策略的用例真机安装 HAP跑一遍冷启动、热重载、断网降级三个场景用hdc抓日志确认没有 method channel 未实现的警告老数据迁移场景用旧包写入数据后升级到鸿蒙包验证迁移逻辑。这套路径跑完我才会认为鸿蒙数据访问层具备上线条件。6.2 我踩过几次坑之后的三个结论第一鸿蒙化改造的难点不在 kiss_repository 这种纯 Dart 库本身而在它四周的依赖裙带。数据层要真正跑起来存储、路径、网络、权限四样都可能出问题任何一样都够你折腾半天。第二先给整个工程做一次平台能力清单再动手把所有调用了原生能力的代码全部列出来比边改边查高效十倍。第三版本对齐永远排第一优先级Flutter 引擎、鸿蒙 SDK、插件版本三者的兼容矩阵必须先确认否则后面所有排障都是在错误的地基上盖楼。6.3 后续还能往哪个方向延伸这次适配做完之后我在想 kuss 风格的数据层还有两个很好的扩展方向。一个是做离线优先把最近请求的响应序列化到本地断网时直接用上次的数据渲染页面配合 kiss_repository 的 CacheFirst 策略特别顺。另一个是给 Repository 加一层请求合并同一时间段内对同一个 key 的并发请求只放行一个其他的等同一个结果返回这在弱网设备上能明显减少重复拉取。鸿蒙真机上的网络环境往往比模拟器复杂这一层优化很值得做。说到底鸿蒙化改造最难的不是某个 API 不会调而是能不能用一套干净的架构把平台的差异全都挡在数据访问层之外。kiss_repository 恰好提供了一个足够薄的边界剩下的工作就是像我在 3.2 和 4.1 里写的那样把插件替换干净、把抽象搭稳剩下的交给时间验证。
返回列表