ARTICLE DETAIL

资讯详情

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

Flutter应用迁移OpenHarmony:Dio网络层与跨平台实战复盘

Flutter应用迁移OpenHarmony:Dio网络层与跨平台实战复盘 最近手头一个 Flutter 项目要往 OpenHarmony 设备上迁移正好产品那边丢过来一个需求做一个猫咪图库应用要求一套代码同时跑在 Android、iOS 和开源鸿蒙上。这类需求以前听着像伪命题但现在 OpenHarmony 官方维护了 Flutter 适配分支配合 Dio 把网络层做好是真的能落地的。这篇文章把我这次实战的完整过程复盘一遍从 OpenHarmony 上 Flutter 开发环境怎么搭到用 Dio 对接猫咪图片 API 实现列表、分页、缓存、详情页再到跨平台适配中踩过的权限和组件通信的坑都会讲到。适合手里有 Flutter 项目想在开源鸿蒙上跑起来、或者单纯想了解 OHOS 上 Flutter 开发流程的开发者参考。1. 项目定位为什么是 OpenHarmony Flutter Dio 这套组合1.1 猫咪图库的本质是一个标准的内容消费应用别被猫咪图库这个题材带偏了这个应用拆开看就是一个标准的 Feed 流海量图片数据、分页加载、缩略图网格、点击看大图、跨页面状态共享。真正有价值的东西不在 UI而是一套稳定的数据获取与缓存链路。猫咪只是载体换成壁纸、设计灵感、表情包这套架构可以直接照搬。我把需求收敛成四个点首页随机的猫咪图片流按数量触底加载更多点击缩略图进入详情页看大图支持简单收藏收藏状态跨页面同步在 OpenHarmony 真机上流畅运行。选这个题材有私心公开猫咪图片 API 完全免费、数据量大、不涉及版权问题拿来验证功能再合适不过。核心要决策的就三件事跨平台框架选型、网络层选型、渲染与数据流设计。三件事分别对应 Flutter、Dio 和缓存的取舍。1.2 为什么不用原生 ArkTS 开发OpenHarmony 的原生语言 ArkTS 配 ArkUI 这套声明式 UI 确实不错跟 Flutter 的 Widget 树思路很像但有个现实问题如果业务代码已经在 Flutter 里写好了从零用 ArkTS 重写一遍等于把 Android 和 iOS 两个平台都丢下只服务 OpenHarmony 一个生态。对大多数团队来说这是最不划算的路径。Flutter 在 OpenHarmony 上跑的意义在于存量复用。社区里已经积累了大量 Dart 包和 Flutter 组件Dio、cached_network_image、provider 这些库在 OHOS 适配版 Flutter 上基本能直接用。再加上渲染引擎是自绘的 Skia/Impeller不依赖系统 WebView 或原生控件跨平台一致性比 React Native 那套桥接方案更可控。ArkTS 也不是没用后面做原生插件、权限配置、PlatformView 嵌入时还是要写 ArkTS 代码。所以正确的姿势是业务层全部 Flutter系统能力层用 ArkTS 写插件两边通过 MethodChannel/EventChannel 通信。这样既保住了跨平台能力又拿到了 OpenHarmony 的系统能力。1.3 Dio 为什么比 http 和 qio 更合适Flutter 生态里网络库就那么几个选择。官方 http 包适合快速验证但生产级应用用起来很别扭没有拦截器、超时配置要靠手动封装、取消请求要自己管理 Completer代码很容易写成一把梭。Dio 的出现基本就是为了解决这些问题拦截器机制、连接超时与接收超时、取消令牌、FormData、自定义适配器全都开箱即用。还有一个新库 qio性能数据确实好看但生态成熟度跟 Dio 差一个量级社区资料少出了问题不太好查。我这次项目核心是图片列表网络层最需要的三个能力是超时控制、日志拦截、统一错误处理Dio 的 InterceptorsWrapper 一个机制全搞定。能力Diohttp 包qio拦截器完整且支持异步需要手写初步支持超时配置连接/接收分别配置仅 connectTimeout支持取消请求CancelToken需要手写支持错误类型统一DioException手动判断有异常体系社区资料多踩坑方案齐全官方轻量少结论很明确跨平台项目里网络层选 Dio基本不需要犹豫。2. 环境搭建把 Flutter 开发环境接到 OpenHarmony 上2.1 必须准备的工具链清单OpenHarmony 上的 Flutter 开发不是装个官方 Flutter SDK 就能干的它有一套独立的分支工具链。我当时准备的东西是这样DevEco Studio用来编译和运行 ohos 侧工程、OpenHarmony SDK、hdc 调试工具以及 OpenHarmony 团队维护的 Flutter SDK 适配版。这个适配版 SDK 跟谷歌官方 Flutter SDK 是分开管理的要在 GitHub 上找 OpenHarmony 对应的 flutter_flutter 仓库 ohos 分支还有配套的 Dart SDK。这里有个容易踩的坑不要用官方flutter pub get拉完依赖就往 OHOS 上跑很多原生插件在 ohos 目录里还没有对应实现会直接编译失败。正确方式是先把 OHOS 分支的 Flutter SDK 配好再用它来创建和构建项目。版本管理上我吃过亏Flutter 适配分支、Dart SDK、DevEco Studio 三者要尽量保持配套最好直接参照 DevEco Studio 里提示的兼容版本。如果混搭编译时经常出现奇怪的 native 符号缺失错误排查起来很费时间。2.2 从零创建一个支持 ohos 平台的项目环境配好后创建工程很简单关键是在flutter create时把平台参数带全。我用的命令大致是这样flutter create --platforms android,ios,ohos cat_app创建完会发现项目根目录多了一个ohos文件夹这就是 OpenHarmony 原生工程壳子结构跟 Android 的android目录类似。第一次接触可能会觉得这个目录多余但 Flutter 在 OHOS 上的运行机制决定了必须有这个壳来做平台层引导。之后用 DevEco Studio 打开ohos目录等待 Gradle 同步完成。这里注意不是所有 Gradle 任务都能在命令行里直接跑有部分操作必须靠 DevEco Studio 的侧栏工具完成比如签名配置。真机调试前还要确保设备已开启开发者模式并且用hdc list targets能看到设备。我在这一步卡过最久的是 hdc 和 adb 的混淆。OpenHarmony 用的是 hdc不是 adb如果习惯性敲adb devices会发现设备永远连不上。确定 hdc 能识别设备后再回 Flutter 工程执行flutter run -d 设备ID就能跑起来。2.3 网络权限OpenHarmony 的权限声明和 Android 完全不同这是新手最容易忽略的点。Android 里网络权限写在AndroidManifest.xmlOpenHarmony 里则是ohos目录下的module.json5。如果不声明ohos.permission.INTERNET应用可以正常启动但所有网络请求都会静默失败图片区域一片空白。{ module: { name: entry, requestPermissions: [ { name: ohos.permission.INTERNET } ] } }权限配好之后还要确认应用是否开启了 HTTPS 强制校验。默认情况下 OpenHarmony 对明文 HTTP 的管控跟 Android 9 类似如果 API 域名不是 HTTPS需要在网络配置文件里放行。我这次用的猫咪图片 API 是 HTTPS所以只加了 INTERNET 权限就通了但如果接内网地址或者测试环境 HTTP 接口这一步必踩。3. 数据层设计Dio 封装与猫咪图片 API 对接3.1 先定义数据模型别把网络响应直接塞给 UI很多 Flutter 新手习惯把 Map 直接丢到 Widget 里网络层和 UI 层耦合在一起后期分页、收藏、缓存全都会变得很难维护。我第一步永远是建模型。猫咪图片接口返回的 JSON 长这样[ { id: abc123, url: https://cdn2.thecatapi.com/images/abc123.jpg, width: 1024, height: 768 } ]对应的 Dart 模型就很简单class CatItem { final String id; final String url; final int width; final int height; CatItem({required this.id, required this.url, required this.width, required this.height}); factory CatItem.fromJson(MapString, dynamic json) { return CatItem( id: json[id] as String, url: json[url] as String, width: json[width] as int? ?? 0, height: json[height] as int? ?? 0, ); } }模型里把宽高提前解析出来有实际意义网格布局需要根据图片宽高比预留占位避免图片加载过程中列表疯狂跳动。如果等图片下载完才知道宽高用户的浏览体验会非常差。3.2 Dio 实例的初始化参数不是随便填的我用的是 TheCatAPI 的免费搜索接口地址是https://api.thecatapi.com/v1/images/search。这个接口支持limit和page参数正好配合分页。Dio 实例初始化时我把超时时间、基础 URL、公共请求头都放在 BaseOptions 里final dio Dio(BaseOptions( baseUrl: https://api.thecatapi.com/v1, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { x-api-key: const String.fromEnvironment(CAT_API_KEY), }, ));这里有个细节API Key 不要硬编码到源码里Flutter 支持--dart-defineCAT_API_KEYxxx在构建时注入。代码仓库被拉出去的时候Key 不会跟着泄露。另外connectTimeout和receiveTimeout我故意分开设置因为图片弱网场景下连接很慢但接收一旦开始就应该稳定两者混用一个值容易误判超时。Dio 默认的响应类型是 JSON但我在做图片接口时主动指定了List类型的响应数据因为接口返回的是数组而不是对象。这一步不做的话resp.data会被解析成Listdynamic后面强转Map会直接抛类型错误。3.3 拦截器日志、错误统一处理和重试策略Dio 的拦截器是它最值钱的设计。我写了一个简单的拦截器把请求方法和 URL 打出来错误时把状态码和错误信息也打出来开发阶段排查问题能省一半时间dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { debugPrint([CatApp] ${options.method} ${options.uri}); handler.next(options); }, onResponse: (response, handler) { debugPrint([CatApp] ${response.statusCode} ${response.realUri}); handler.next(response); }, onError: (DioException e, handler) { debugPrint([CatApp] error: ${e.type} ${e.message}); handler.next(e); }, ));handler.next一定不能漏。漏掉之后请求会一直挂在拦截器里页面转圈转半天。重试逻辑我没有单独引入 retry 库而是在业务层 catch 到DioExceptionType.connectionTimeout和receiveTimeout后做一次重试图片列表场景偶尔一次超时是正常的直接重试比弹错误提示框体验好得多。3.4 分页策略与图片缓存猫咪图片列表我用的是最经典的 page limit 策略FutureListCatItem fetchCats({int page 1, int limit 30}) async { final resp await dio.get(/images/search, queryParameters: { limit: limit, page: page, has_breeds: false, }); return (resp.data as List) .map((e) CatItem.fromJson(e as MapString, dynamic)) .toList(); }细节在 UI 层后面会说。图片缓存这里不要用Image.network裸奔它没有任何本地持久化能力每次滑回来都要重新下载。我用的是cached_network_image包它内部做了内存和磁盘两级缓存。同一个 URL 第二次展示时直接从磁盘读滚动列表的体验完全不一样。4. UI 层实现图库页面与关键交互4.1 首页网格布局别用 ListView用 GridView.builder图片流用列表还是网格我选了网格。猫图比例接近方形三列网格一屏能展示九张信息密度比单列大得多。代码上用GridView.builder配合SliverGridDelegateWithMaxCrossAxisExtent这样在不同屏幕宽度下都能自动调整列数平板和手机不用写两套布局GridView.builder( controller: _scrollController, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 180, childAspectRatio: 1, crossAxisSpacing: 4, mainAxisSpacing: 4, ), itemCount: _cats.length 1, itemBuilder: (context, index) { if (index _cats.length) { return const Center(child: CircularProgressIndicator()); } return CatThumbnail(cat: _cats[index]); }, )这里itemCount比数据多 1最后一项显示加载指示器。触底加载时不用额外加底部空隙结构上很干净。4.2 下拉刷新与触底加载的连带问题下拉刷新直接用 Flutter 官方的RefreshIndicator套在GridView外面就行没有太多技术含量。真正麻烦的是触底加载和刷新并发时的数据竞争。我见过很多项目触底加载时会重复请求同一页或者刷新过程中还在加载下一页导致列表出现重复图片。解决办法是老一套但很有效用_isLoading标志位加锁。触底回调里如果_isLoading为 true 就直接 return请求开始前置 true请求结束无论成功失败都置 false。刷新时把页码归 1清空列表再重新拉第一页。_scrollController.addListener(() { if (_scrollController.position.pixels _scrollController.position.maxScrollExtent - 200) { _loadMore(); } });触底判断的阈值我留了 200 像素在图片还没滑到底的时候就提前加载减少用户看到加载圈的频率。4.3 图片加载与缓存细节决定流畅度缩略图不要直接加载原图。猫咪图片接口返回的url是原始高清图直接用CachedNetworkImage加载的话三列网格同时下载十几张几 MB 的原图内存和流量都扛不住。我做了一层裁剪缩略图统一拉到 300 像素宽CachedNetworkImage( imageUrl: cat.url, width: 180, height: 180, fit: BoxFit.cover, memCacheWidth: 360, memCacheHeight: 360, placeholder: (context, url) Container( color: Colors.black12, child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ), errorWidget: (context, url, error) const Icon(Icons.broken_image_outlined), )注意memCacheWidth和memCacheHeight这是很多项目忽略的性能开关。CachedNetworkImage默认按原始分辨率解码图片进内存一张 2000x1500 的图解码后在内存里接近 12MB。把缓存宽高限制到 360内存占用可以降到几十 KB 级别滚动时的 GC 卡顿会明显减少。4.4 详情页与路由状态Navigator 切换页面会丢状态吗详情页用Navigator.push跳转传CatItem对象过去。这里要澄清一个常见困惑Navigator.push默认不会销毁前一页的 State页面只是被压到栈下面状态还在。真正丢状态的是 Tab 切换或者使用了非标准的返回逻辑。我的做法是在详情页里收藏猫图这个状态存到全局的ChangeNotifier里返回首页后首页通过ListenableBuilder自动更新 UI。路径是用户在看大图时点收藏返回列表页时缩略图右上角的小红心已经亮了。这个流程用Navigator.push完全可以做到不需要任何特殊处理。如果担心页面被系统回收可以给首页 State 加AutomaticKeepAliveClientMixin让列表在路由栈中保持存活。在图片列表这种高频滚动场景这个 mixin 值得加。5. 跨平台适配OpenHarmony 系统级能力接入5.1 Android 和 OpenHarmony 的权限模型差异Flutter 层写 API 权限请求通常用 permission_handler 插件但要清楚这个插件在 OpenHarmony 上支持得并不完整。OpenHarmony 的权限模型有自己的体系运行时权限和安装时权限分类跟 Android 不完全一致调用方式也不同。这里给一个最实用的原则Flutter 侧不要依赖任何平台特定的权限声明逻辑权限配置全部下沉到 ohos 原生工程里完成。比如 INTERNET 这种基础权限直接改module.json5不经过 Dart 代码。涉及相机、地理位置等运行时权限需要开发对应的 ArkTS 插件用 Flutter 的标准插件机制暴露方法给 Dart 层。5.2 PlatformView在 OpenHarmony 上嵌入原生视图跨平台应用偶尔需要嵌入原生控件比如视频播放器、地图。Flutter 给这类需求提供了 PlatformView 机制。在 OpenHarmony 上原生侧视图是用 ArkTS 实现的再通过 Flutter 官方定义的 PlatformView 注册表暴露出来。我在这个项目中先用一个简单的场景练了手嵌入了一个原生按钮目标是把收藏按钮替换成原生的点击样式。这本身没什么业务价值但完整跑通了原生视图从注册、创建到 Flutter 侧引用的链路。关键步骤是在 ohos 工程里实现一个继承自 PlatformView 的类并注册到 PlatformViewRegistry 中。实际测试中发现OpenHarmony 的 PlatformView 性能和 Android 平台相当没有明显的掉帧问题。但如果有图片列表这种高频滚动场景嵌入原生视图的数量要控制一次屏幕内的 PlatformView 最好不要超过个位数否则合成压力会变大。5.3 MethodChannel 与 EventChannelFlutter 和 ArkTS 的双向通信组件通信是这个项目的必修课。我从 Flutter 侧调原生获取设备信息时用了 MethodChannelDart 侧代码static const MethodChannel _channel MethodChannel(cat_app/device); final String device await _channel.invokeMethod(getDeviceInfo);对应的 ArkTS 侧要在 MainAbility 或者 PageAbility 里注册这个 channel实现onMethodCall逻辑。这里有个坑channel name 必须完全一致包括包名形式的命名空间Dart 侧写错一个字符调用时就会报 MissingPluginException。监听原生主动发来的数据比如系统电量变化就轮到 EventChannel 出场。Flutter 侧用receiveBroadcastStream订阅static const EventChannel _event EventChannel(cat_app/events); _event.receiveBroadcastStream().listen((event) { debugPrint([EventChannel] $event); }, onError: (e) debugPrint([EventChannel] error $e));EventChannel 是单向的流原生侧负责 pushDart 侧只负责监听。方向感一定要清晰想从 Dart 往原生发指令时不要用 EventChannel用 MethodChannel。5.4 组件通信与状态共享收藏功能的跨页面同步猫咪收藏功能涉及三个页面列表页、详情页、收藏页。状态不能散落在单个 Widget 里我用了一个全局的FavoritesStore继承ChangeNotifierclass FavoritesStore extends ChangeNotifier { final SetString _ids {}; bool has(String id) _ids.contains(id); void toggle(String id) { if (_ids.contains(id)) { _ids.remove(id); } else { _ids.add(id); } notifyListeners(); } }这个 store 在应用启动时创建通过构造参数注入给首页和详情页。收藏操作后调用notifyListeners()所有监听了这个 store 的 Widget 都会重建。这种轻量级状态管理方案在 Flutter 里的好处是不引入多余概念调试时直接看调用栈就能定位问题。在试过 Riverpod 和 Bloc 之后我反而觉得 ChangeNotifier 在这个体量的项目里最顺手。复杂状态管理方案本身也是一种负担猫咪图库这种单数据源场景根本用不上那么多高级特性。6. 实战中的常见问题与排查技巧6.1 编译和打包阶段的坑OpenHarmony 的 Flutter 项目编译失败排在第一位的原因就是 SDK 版本不匹配。我遇到过一次 Flutter 适配版和 DevEco Studio 内置的 ArkTS SDK 版本冲突报错信息指向的是main gradle plugin实际问题是 gradle wrapper 版本和插件不兼容。排查时先看ohos/build.gradle里的依赖版本再看 local.properties 里的 SDK 路径。打包时如果遇到 Java 异常和could not close input stream这类错误通常不是代码问题而是 Gradle 缓存损坏。先执行./gradlew clean再重新构建。这种缓存类问题在 CI 机器上尤其常见本地一次成功但打包机必失败时优先怀疑缓存而不是代码。6.2 图片加载失败先查权限再查域名我调试过程中图片大面积加载不出来的根因是module.json5里漏了 INTERNET 权限。这个问题的特征是日志里 Dio 请求已经发出去了但响应直接抛 SocketException。如果你看到Failed host lookup基本可以断定是网络权限或者 DNS 问题。还有一种情况是 HTTPS 证书校验失败如果 API 的证书链不完整Dio 会直接拒绝连接。开发阶段可以临时在 Dio 的 HttpClientAdapter 里关闭证书校验但生产环境绝对不能这么做否则会引入中间人攻击风险。6.3 列表滚动卡顿先查图片解码再查 PlatformView图片列表滚动的卡顿绝大部分不是 CPU 问题而是图片解码占用的内存峰值太高。前面说的memCacheWidth参数建议所有图片组件都配置上。另外不要在大列表里给每张图片都套 Hero 动画Hero 会阻止图片在滚动中被回收内存压力会持续累积。如果滚动时掉帧特别严重可以用 Flutter DevTools 的 Performance 面板看 Raster 线程耗时。Raster 线程满则说明图片合成压力大优先降低图片解码尺寸UI 线程满则说明 Widget 重建频繁检查是不是 store 通知触发了整个列表重建。6.4 Navigator 切换后状态丢失的根因和解决如果你发现Navigator.push回来之后列表位置丢失或者状态重置大概率不是路由本身的问题而是页面在路由栈里被回收了。排查思路是这样给页面 State 加上AutomaticKeepAliveClientMixin并让wantKeepAlive返回 true。如果加了还丢检查是不是父级用了会销毁子树的结构。另外注意用TabBarTabBarView时默认切换 Tab 会销毁不可见页面的 State。如果收藏页和首页之间是 Tab 关系也需要AutomaticKeepAliveClientMixin或者改用IndexedStack保持页面存活。这是我实际做完收藏同步功能后才知道的细节文字描述起来很轻巧但调试时特别容易忽略。这个项目做完我最大的感受是 OpenHarmony 上的 Flutter 已经从能跑 Hello World走到了能跑完整业务的阶段。整套开发流程从环境搭建、工程创建、Dio 网络层封装到权限和原生通信本质上跟 Android 平台的 Flutter 开发差不了太多真正需要额外花时间的反而是平台适配细节。最后分享一个自己的习惯每次准备在 OHOS 上做新功能前先翻一遍 OpenHarmony flutter_flutter 仓库的 issue 列表看看有没有人已经踩过同类的坑。跨平台开发到最后比的不是谁 API 用得熟而是谁对平台差异心里有数。猫咪图库只是一个小小的起点换成生产级的业务应用这套方法论照样能打。
返回列表