ARTICLE DETAIL

资讯详情

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

Flutter+Dio实战:跨平台猫咪图库在OpenHarmony上的适配

Flutter+Dio实战:跨平台猫咪图库在OpenHarmony上的适配 【OpenHarmony】实战打造跨平台猫咪图库应用——flutter_dio_cat_app国庆假期闲不住把手头一个练手项目重新翻了出来一个跨平台的猫咪图库 App取名flutter_dio_cat_app。核心目标很纯粹——一套 Flutter 代码同时跑在 OpenHarmony 设备、Android 和 iOS 上用 Dio 从公开猫咪图片接口拉数据做一个能刷、能搜、能收藏的猫咪图库。这篇文章把我从环境搭建到真机运行的完整过程记录下来包括踩过的坑、改过的配置、以及最后在 OpenHarmony 设备上的实际表现。如果你是正在折腾 Flutter 跨端开发、又想往 OpenHarmony 生态上靠的开发者或者你只是好奇Flutter 到底能不能在鸿蒙设备上好好跑这篇应该都能给你一点参考价值。我尽量把每个步骤背后为什么这么做的原因也讲清楚避免照着抄都不知道自己在抄什么的情况。1. 为什么猫咪图库选了 Flutter Dio 这套组合先说选型。这个项目最初的目的就是为了验证一件事OpenHarmony 设备在 Flutter 生态下到底能不能做到一套代码多端复用。所以技术栈的选型不是拍脑袋定的而是围绕这个目标来的。Flutter 这边主要图它三点自绘引擎UI 不依赖原生控件天然具备跨端一致性。同样的渲染逻辑在 OpenHarmony 上跑得到的是和 Android 端几乎一致的效果。状态管理、网络库、图片缓存等生态组件相对成熟Dio 就是其中最典型的网络层方案。社区里有现成的 OpenHarmony 适配分支和工程模板省去从零移植的绝望感。Dio 在其中的位置就是统一接管所有 HTTP 请求猫咪图片列表、搜索、随机图片这些接口全都走 Dio。选择 Dio 而不是直接用http是因为这个项目的网络诉求并不只是发个 GET 请求那么简单需要拦截器统一打日志、统一加请求头方便排查问题。需要超时控制、重试机制移动端网络环境不能太乐观。需要支持取消请求用户快速滑动列表时上一批图片请求可以被取消掉避免流量浪费。等一下有人可能会问OpenHarmony 应用层主流是 ArkTS/ArkUI为什么不直接用 ArkTS 写非要绕一圈 Flutter这个问题的答案恰恰是这个项目的意义所在。ArkTS 生态目前更适合纯 OpenHarmony 设备的原生开发但如果团队已经有 Flutter 代码库、或者有 iOS/Android 多端交付压力那么用 Flutter 做业务层、向下兼容 OpenHarmony 反而是更实际的路径——业务逻辑写一次三端都能交付。这个项目就是模拟这种真实场景核心代码全部放在 Flutter 层OpenHarmony 只当它是宿主环境之一。再说个大家关心的对比ArkTS 和 Flutter 哪个更流行在当前阶段Flutter 的全球社区体量和第三方库数量仍然占优ArkTS 则在 OpenHarmony 原生业务上有系统级优势。两者不是替代关系是不同场景的选择。我选择 Flutter本质上是因为我想把跨平台的效率最大化而不是只服务单一种类设备。2. OpenHarmony 侧的环境准备与工程骨架搭建2.1 工具链版本匹配是我踩的第一个坑OpenHarmony 的 Flutter 开发和 Android 开发有个很大的区别工具链跟官方发布节奏挂钩。不能简单flutter create完事需要关注 Flutter SDK 的版本是否为 OpenHarmony 适配分支。我最终确定的一套版本组合是这样的工具版本 / 说明OpenHarmony SDKAPI 9 及以上我这边的设备是 API 10Flutter SDKOpenHarmony 适配分支的 3.7.x 版本线Dart SDK随 Flutter 分支自带DevEco Studio用于构建 OpenHarmony 工程和安装应用项目结构Flutter 工程 ohos目录承载 OpenHarmony 壳工程这里的关键点是OpenHarmony 的 Flutter DevTools、Provider 等纯 Dart 包都照常可用但凡是依赖原生插件的功能得确认插件有没有对应的 OpenHarmony 适配版。我的项目里网络层完全走 Dart图片加载用cached_network_image Flutter 自绘避开了绝大部分原生插件兼容问题。2.2 初始化工程结构我建了个 Flutter 工程然后按 OpenHarmony 适配模板往里补ohos壳工程目录。完整的骨架大概是这样flutter_dio_cat_app/ ├── lib/ │ ├── main.dart # 入口配置路由与 Provider │ ├── api/ │ │ ├── api_client.dart # Dio 封装 │ │ └── cat_api.dart # 猫咪接口定义 │ ├── models/ │ │ └── cat_image.dart # 数据模型 │ ├── providers/ │ │ └── gallery_provider.dart # 图库状态管理 │ └── pages/ │ ├── gallery_page.dart # 图片瀑布流 │ └── detail_page.dart # 大图详情 ├── ohos/ # OpenHarmony 壳工程 │ ├── entry/ │ └── build-profile.json5 └── pubspec.yamlohos目录的作用就是把 Flutter 产出的产物libflutter.so、Dart 业务代码打包成 OpenHarmony 的 HAP 应用。这个目录里的源码主要做两件事初始化 Flutter 引擎并加载页面以及处理系统返回键、生命周期等原生事件。在搭建壳工程时我踩了一个印象深刻的坑build-profile.json5里配置的签名信息不对导致安装时报签名错误后面专门开了一节讲。2.3 Flutter 模块与 ohos 壳工程的关联逻辑你可能会好奇Flutter 代码到底是怎样跑进 OpenHarmony 设备里的简单说OpenHarmony 的 Flutter 适配方案本质上是把 Flutter 引擎当作一个 Native 组件嵌入到 OpenHarmony 应用里。ohos壳工程是一个标准 OpenHarmony HAP 工程它会加载 Flutter 引擎、注册 Flutter 视图组件然后把 Dart 代码编译出来的产物和引擎一起打包。运行时Dart 层负责所有 UI 绘制和业务逻辑OpenHarmony 层只提供窗口、输入事件、传感器等系统能力。这种壳 引擎的架构决定了我们平时写 Flutter 代码时几乎不用关心 OpenHarmony 的 ArkUI 语法只要保证 Dart 侧代码不触碰不兼容的原生能力即可。3. 数据层把 TheCatAPI 的请求链路拧顺这个项目用的公开接口是 TheCatAPI。数据访问层我全部用 Dio 实现并且做了一套比较完整的网络防御体系。3.1 Dio 实例的初始化方式先看api_client.dart里的核心配置import package:dio/dio.dart; class ApiClient { static Dio get dio { final dio Dio(BaseOptions( baseUrl: https://api.thecatapi.com/v1, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), sendTimeout: const Duration(seconds: 10), headers: { Content-Type: application/json, Accept: application/json, }, queryParameters: { api_key: 你的KEY, limit: 20, }, )); dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: false, logPrint: (obj) debugPrint(obj.toString()), )); return dio; } }这里有个容易被忽略的细节queryParameters里的api_key对所有请求统一生效就不用每个接口单独带 key 了。而LogInterceptor固定打在 Debug 环境方便我确认每个请求到底发了什么出去、服务端回了什么状态码。3.2 接口定义与数据模型猫咪图库的信息取了两个接口获取随机猫咪图片列表、按关键词搜索猫咪图片。接口方法统一收到CatImage模型里class CatImage { final String id; final String url; final int width; final int height; CatImage({ required this.id, required this.url, required this.width, required this.height, }); factory CatImage.fromJson(MapString, dynamic json) { return CatImage( id: json[id] as String? ?? , url: json[url] as String? ?? , width: json[width] as int? ?? 0, height: json[height] as int? ?? 0, ); } }fromJson里做空值兜底有一个实际原因猫咪图片接口偶尔会返回缺失字段的数据如果直接解构赋值一个空字段就能让整个列表渲染崩溃。空值兜底配合后面的容错逻辑把这类偶发问题吞在数据层里不污染 UI。3.3 请求封装错误处理与取消机制网络请求不能裸写在页面里我封装了一层class CatApi { static FutureListCatImage fetchImages({int page 1, int limit 20}) async { try { final response await ApiClient.dio.get( /images/search, queryParameters: { page: page, limit: limit, }..addAll(ApiClient.baseQueryParams), ); if (response.statusCode 200) { final list response.data as List; return list.map((e) CatImage.fromJson(e)).toList(); } return []; } on DioException catch (e) { debugPrint(fetchImages error: ${e.type} — ${e.message}); return []; } } }这里用 DioException 按类型细分了错误场景connectionTimeout是连接超时、connectionError是断网、badResponse是服务端异常返回。页面层不需要知道这些细节只需要知道这次请求返回了空然后展示重试入口。值得说一下取消机制。Dio 支持通过CancelToken取消请求我在无限滚动场景里当组件销毁或者用户快速上拉时会取消旧的请求避免网络回调把状态写进已注销的 WidgetCancelToken _cancelToken CancelToken(); void _loadMore() { _cancelToken.cancel(cancel old); _cancelToken CancelToken(); CatApi.fetchImages(...).catchError(...); }3.4 缓存策略让图库在弱网环境也能玩直接用cached_network_image做图片缓存数据层则自己实现了一个轻量内存缓存最近 3 页的图片列表缓存在内存里上拉加载时优先取缓存缓存没有命中才发新请求。这个策略的逻辑很简单刷图库是高频操作用户往上滑回之前的页面时直接从缓存渲染速度为 0 毫秒。真机测试时我试过把网络关掉再打开 App列表依然能显示最近浏览过的图片内存缓存 图片磁盘缓存虽然不能再加载新图但整体体验不至于变成一块白板。这个兜底效果在弱网场景下特别有用。4. 图片瀑布流 UI 与 Provider 状态管理的落地方式4.1 为什么用 Provider 而不是 setState猫咪图库的页面状态至少包括图片列表、加载状态、是否正在加载更多、当前关键词。如果用setState管理这些状态分散在页面的各个方法里一旦加页面跳转传参、搜索联动代码会迅速膨胀。用 Provider 的好处是把数据状态和展示页面拆开。GalleryProvider统一维护列表数据与加载状态页面只负责监听变化并驱动 UI。先看 Provider 的核心结构class GalleryProvider extends ChangeNotifier { final ListCatImage _images []; bool _isLoading false; int _page 1; ListCatImage get images List.unmodifiable(_images); bool get isLoading _isLoading; Futurevoid loadMore() async { if (_isLoading) return; _isLoading true; notifyListeners(); final more await CatApi.fetchImages(page: _page, limit: 20); if (more.isNotEmpty) { _images.addAll(more); _page; } _isLoading false; notifyListeners(); } void clear() { _images.clear(); _page 1; notifyListeners(); } }List.unmodifiable(_images)是为了防止页面代码直接修改列表内部数据所有数据变更都走 Provider 的方法保持数据流单向。4.2 页面里怎么正确监听状态入口处用MultiProvider统一注册void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) GalleryProvider()), ], child: const CatApp(), ), ); }列表页里用context.watchGalleryProvider()获取状态final gallery context.watchGalleryProvider(); return Scaffold( appBar: AppBar(title: const Text(猫咪图库)), body: NotificationListenerScrollNotification( onNotification: (notification) { if (notification.metrics.pixels notification.metrics.maxScrollExtent - 200) { gallery.loadMore(); } return false; }, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, ), itemCount: gallery.images.length, itemBuilder: (context, index) { return CatImageCard(image: gallery.images[index]); }, ), ), );无限滚动我用的是NotificationListener 距离底部 200 像素时触发加载。为什么不直接用ScrollController因为在 GridView 里NotificationListener能更统一地捕获到滚动事件判断逻辑在通知层完成不需要挂额外的 controller。4.3 等待、错误、空数据三态切换真机调试时遇到过这类情况弱网下拉取图片接口超时返回空列表页面直接显示空白用户完全不知道发生了什么。所以页面里必须做三态切换加载中显示居中的 CircularProgressIndicator。加载失败 / 返回空显示空状态卡片给一个重新加载按钮。有数据正常瀑布流。GalleryProvider增加一个_hasError字段bool get hasError _hasError; Futurevoid refresh() async { _hasError false; clear(); await loadMore(); }页面根据gallery.isLoading gallery.images.isEmpty判断首屏加载态根据gallery.hasError gallery.images.isEmpty判断错误态。这样就算 Dio 返回空页面也有明确的反馈路径。4.4 组件通信的几个顺手实践项目中组件通信主要体现在这三个层面页面与状态仓库通过 Provider 的watch/read。父组件与子组件用构造函数传参例如CatImageCard(image: ...)。列表项与详情页通过Navigator.push传对象引用详情页直接读对象字段。这里提醒一句组件通信如果层级深了优先考虑 Provider/其他状态方案别再一层层手动传回调否则后期维护会崩溃。5. 真机适配阶段从崩溃到流畅的调优过程5.1 安全区域与异形屏OpenHarmony 设备有的带挖孔、有的带圆角如果不处理安全区域图片卡片会被摄像头区域遮挡AppBar 也可能顶到状态栏上。解决办法很简单页面主内容外层包一个SafeArea。return SafeArea( child: Scaffold( body: ... ), );重点说一下SafeArea应该放在Scaffold外面还是里面实测下来放在外面效果更可控因为 OpenHarmony 上Scaffold的appBar高度并不总是等于状态栏高度外层包一层能让整个布局避开系统区域。5.2 分辨率适配与图片卡片尺寸OpenHarmony 设备的分辨率分布比手机更杂有的平板 10 英寸有的是竖屏手机形态。我的 GridView 固定 2 列在不规则大屏上卡片尺寸比例会很怪。后来我做了一个简单适配根据像素宽度动态决定列数宽度超过 600 用 3 列超过 900 用 4 列否则 2 列final crossAxisCount constraints.maxWidth 900 ? 4 : constraints.maxWidth 600 ? 3 : 2;用LayoutBuilder包裹 GridView 就能读到实际宽度。这个写法比写死尺寸优雅很多。5.3 内存峰值调优解决图片列表滑动卡顿真机测试最明显的问题是列表快速滑动时内存占用飙升卡片出现白屏闪烁。排查后定位到根因——图片加载未做尺寸压缩。WhatCatAPI 返回的图片原图宽高很大直接加载原图对内存是巨大压力。解决方案是cached_network_image的memCacheWidth参数CachedNetworkImage( imageUrl: image.url, memCacheWidth: 400, placeholder: (context, url) Container(color: Colors.grey[200]), errorWidget: (context, url, error) const Icon(Icons.broken_image), )memCacheWidth: 400表示内存缓存只保留宽度 400 像素的缩放版本而不是原图。这一个小改动内存峰值肉眼可见地降了下来滑动也流畅了很多。这里的原理很直白图片从网络拿到后先解码解码出的位图大小 宽度 × 高度 × 4 字节RGBA。一张 1200×800 的图片解码后大约占用 3.8 MB 内存而缩放到 400 宽之后内存直接降到几百 KB。列表里几十张图差距就非常可观了。5.4 生命周期与后台恢复OpenHarmony 设备和手机一样App 切到后台再恢复进程可能会被系统回收。为了验证这种情况我做了个测试图库加载几十张图后切后台过几分钟再切回来App 没崩溃但列表被系统重建了。这是因为 Flutter 引擎重建后页面 state 被重置。处理方式是让 Provider 的状态在 App 级别持有页面重建后直接从 Provider 读已有数据不需要重新请求接口class CatApp extends StatelessWidget { const CatApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: 猫咪图库, theme: ThemeData( colorSchemeSeed: Colors.orange, useMaterial3: true, ), home: const GalleryPage(), ); } }Provider 注册在runApp的MultiProvider里Widget 树重建时 Provider 实例还在所以列表数据不会丢。这也是选择全局状态方案而不是页面内StatefulWidget的一个重要原因。6. 构建打包阶段的高频报错与处理记录6.1 Flutter Gradle 插件以一种命令式的方式被应用这个报错我在构建 OpenHarmony 壳工程时遇到过You are applying Flutters main Gradle plugin imperatively using the apply method这个问题的根源是工程里某个 Gradle 文件用了老式apply方式引入 Flutter 插件而当前版本的 Flutter Gradle 插件要求用plugins {}块声明式方式引入。我用下面的方式解决plugins { id com.android.application id org.jetbrains.kotlin.android id dev.flutter.flutter-gradle-plugin }需要注意的是这个报错有时也会因为 Flutter SDK 路径配置错误而出现。排查时先看local.properties里的flutter.sdk是否指向正确的 SDK 目录。6.2 Dart VM 初始化失败Unhandled Exception另一个高频问题报错长这样E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: MissingPluginException这个报错的真实原因通常是Flutter 工程里pubspec.yaml声明了某个插件但 OpenHarmony 壳工程里没有对应的原生插件实现。Dart 层调用时找不到原生通道就抛MissingPluginException。我的解决方案是把项目依赖收敛到纯 Dart 包。比如图片选择用image_picker在 OpenHarmony 上没有适配版就会炸。后来我换成了 Dart 侧实现 系统相册介质库的方式或者干脆不用相机相关能力只展示网络图片。排查这类问题有个小技巧在main()里直接打点debugPrint(PluginManager().getAllPlugins())能快速看到哪些插件被成功注册。6.3 安装阶段签名错误OpenHarmony 真机安装应用时如果 HAP 是用调试证书签的但设备已开启严格校验会提示签名错误。解决办法是在 DevEco Studio 中重新生成调试证书并确保项目的build-profile.json5中签名配置正确。如果设备开启了仅安装可信来源应用需要先在设置中临时关闭相关校验安装完成后再打开。这里有个细节值得注意OpenHarmony 的调试证书是有有效期通常一年的过期后如果不重新生成会一直报安装失败而且报错信息不会提示证书过期只会提示安装失败 / 签名错误。所以遇到这类问题优先检查证书有效期。6.4 异步任务在 Flutter 引擎销毁后回调切后台时如果刚好有请求回调偶尔会看到这样的报错状态在引擎销毁后更新导致 UI 异常。解决方式是GalleryProvider的loadMore里加一步判断if (!hasListeners) return; // 没有监听者说明页面已销毁或者在页面dispose()里显式取消请求。7. 实测效果与可扩展方向按上面这些步骤跑通后整套工程在 OpenHarmony 真机上的表现算是符合预期冷启动到首页首屏图片出现耗时 1.5 秒左右。快速滑动 50 张图片无明显掉帧内存峰值控制在 300 MB 以内。断网再访问能加载缓存图片并显示正确错误提示。同一代码库跑 Android 模拟器和 iOS 模拟器无额外改动逻辑完全复用。如果说有什么遗憾那就是 OpenHarmony 上的系统能力比如调用系统相机、读取相册目前还需要原生插件做针对性的适配不是所有 Flutter 插件都能直接跑起来。这个问题的本质是插件生态的 OpenHarmony 适配进度还没完全跟上和 Flutter 本身的跨端能力无关。后续如果想继续扩展我列了几个现实可行的方向加入收藏功能用本地数据库存储收藏列表数据层可以把 Dio 换成带缓存的dio_cache_interceptor减少重复请求。做一个猫咪品种搜索落地页把 TheCatAPI 的品种接口也接入。把图片加载换成extended_image支持手势缩放、旋转给详情页加一点可玩性。这个项目的完整代码我已经整理到仓库里了里面包含了本文提到的所有配置和最终的目录结构。希望这篇文章能帮你少踩几个坑尤其是那些 OpenHarmony 特有的、百度也搜不到明确答案的坑。如果你也在用 Flutter 往 OpenHarmony 上移植应用欢迎交流你踩过的那些更奇怪的报错。
返回列表