☰
Flutter鸿蒙开发实战:集成dio网络库打造游戏列表应用
2026/10/7 20:19:55 网站建设 项目流程

DAY 3 了。从配置 Flutter for OpenHarmony 环境,到真正跑通一个带网络请求的页面,这中间比想象中曲折,也比想象中值得。我这两天花了不少时间踩坑,核心任务就是做一件事:用 Flutter 在 OpenHarmony 上写一个游戏列表应用,通过 dio 网络请求库拉取数据并展示出来。这个标题看起来平平无奇,但实际动手时会发现,引擎适配、平台权限、网络层封装、状态管理,每一环都藏着独立的坑。

这篇文章主要写给正在学习 Flutter for OpenHarmony(社区常叫 FLOH)的开发者,尤其是从 Android/iOS 跨过来的朋友。读完你会知道怎么配置开发环境、怎么把 dio 集成进鸿蒙 Flutter 工程、怎么处理鸿蒙特有的网络权限和证书问题,以及我实测下来必踩的几个坑。目标很明确:你照着这个流程,能完整跑起来一个具备网络请求能力的 Flutter 游戏列表项目。

1. 先搞明白:Flutter 凭什么能跑到 OpenHarmony 上

1.1 Flutter 引擎的 OpenHarmony 移植原理

Flutter 能跑在 OpenHarmony 上,核心原因不是 Google 官方支持了鸿蒙,而是 OpenHarmony 社区维护的 flutter_flutter 分支一直在做引擎移植。Flutter 的 Dart 代码本身是跨平台的,真正需要适配的是三块:渲染引擎、平台通道、系统能力。渲染引擎解决的是 Skia/Impeller 能否画到 OpenHarmony 的图形栈上;平台通道解决的是 ArkTS 侧怎么接入插件调用;系统能力解决的是网络、文件、权限这些基础服务。OpenHarmony 的 SIG 组把这三块打通以后,我们在 Dart 层写的大部分代码就能直接在鸿蒙上运行。

需要注意一个关键差异:Flutter for OpenHarmony 并不是把 Flutter 跑在 Android 兼容层上,而是跑在 OpenHarmony 的原生图形栈和系统服务上。这就意味着不能把 Flutter 的 Android 适配经验直接照搬。比如网络权限,Android 上要写进AndroidManifest.xml,鸿蒙上得在module.json5里声明。再比如渲染引擎,鸿蒙分支目前默认走 Skia 后端,Impeller 在 OpenHarmony 上还处于早期适配阶段,我在当前版本上尝试开启--enable-impeller没有跑通,所以如果你不是非要尝鲜,用默认渲染路径反而更稳。

1.2 为什么网络层选 dio 而不是 qio

很多人问,OpenHarmony 不是有自己的网络框架 qio 吗,为什么还要引入 dio?这里要分清场景。qio 是面向 ArkTS 开发的应用层 HTTP 框架,API 设计和 Dart 生态完全不搭。我们现在用 Flutter 开发,写的是 Dart 代码,qio 根本没法在 Flutter 的 Dart 层使用。dio 是 Dart 生态里使用最广、文档最全的 HTTP 客户端,支持拦截器、取消请求、FormData、全局配置这些高级能力,而且和 Flutter 的继承关系很好。

从底层链路来看,dio 发请求走的是 Dart 的HttpClient,而HttpClient在 Flutter for OpenHarmony 上会被引擎托管到鸿蒙网络栈。所以只要引擎适配到位,dio 在鸿蒙上基本可以原封不动地使用。我在项目里实测下来,dio 的拦截器机制是最值钱的,统一 Header 注入、错误码透传、请求日志都能在拦截器里一次搞定,后面我会给出一份可以直接抄的封装代码。

2. 环境准备:先把 Flutter for OpenHarmony 跑起来

2.1 SDK 版本选型与工具链匹配

这里先说结论,避免大家走弯路。我当前用的组合是:

  • OpenHarmony SDK 5.0.x 系列(DevEco Studio 5.0 内置)
  • Flutter SDK:flutter_flutter 仓库的 ohos 分支,版本选 3.x 系列
  • JDK:DevEco Studio 自带的 JDK 17

版本选择上有个原则:OpenHarmony SDK 和 flutter_flutter 分支需要保持节奏同步,不要拿 Flutter 官方主分支去跑 ohos 平台。因为 ohos 平台适配是在独立分支上进行的,主分支的新特性未必同步过来。我见过有人直接 clone 官方 Flutter SDK,然后在命令行里加--platforms=ohos,结果flutter create直接报错,原因就是官方 SDK 的模板根本没有 ohos 目录。

正确路径是使用 flutter_flutter 仓库的 ohos 分支,并且在flutter --version里能看到 flutter_flutter 的信息。配置完环境变量之后,可以敲flutter doctor -v验证一下 Flutter 通道和引擎分支,如果显示的版本号和 ohos 分支不一致,后面大概率会报编译错误。

2.2 创建项目并注册 ohos 平台

环境变量配置完之后,创建项目的命令和平时基本一样。我以项目名game_list_app为例:

flutter create game_list_app cd game_list_app flutter create --platforms=ohos .

第二行命令会往已有项目里补上 ohos 平台目录。执行完之后,项目根目录会出现ohos/文件夹,里面是鸿蒙侧的工程文件,逻辑上等同于 Android 项目里的android/目录。

然后打开 DevEco Studio,注意不是打开整个 Flutter 项目,而是单独打开ohos子目录。如果直接 open 整个项目根目录,DevEco 的工程识别会出问题,Gradle 或 Hvigor 同步时经常莫名其妙报错。打开ohos目录后,等待 Hvigor 同步依赖,这一步首次会很慢,因为要下载不少构建工具。

2.3 首次运行:设备连接与调试

工程同步完成后,可以用flutter run -d <device>在鸿蒙设备或模拟器上启动应用。不过在flutter devices里,鸿蒙设备会显示为 ohos 类型,如果你的设备没被识别到,先检查 OpenHarmony SDK 路径和环境变量是否配对。

DevEco 打开ohos目录后,我习惯先用 DevEco 自带的模拟器运行一次,确认 ArkTS 壳工程能编译通过,再用flutter run跑 Dart 代码。这个顺序很重要,因为如果壳工程自身都编译不过,问题大概率出在 SDK 或工程配置上,而不是你的 Dart 代码。

第一次跑通后你可能会发现,Flutter 应用的启动速度比 Android 上慢一些。这是正常的,因为鸿蒙模拟器的图形栈和 Flutter 桥接层还在持续优化中。只要页面能起来,后面开发效率就会高很多。

3. 集成 dio:从依赖到网络层封装

3.1 配置依赖与网络权限

在pubspec.yaml里加入 dio 和 provider:

dependencies: flutter: sdk: flutter dio: ^5.7.0 provider: ^6.1.2

dio 5.x 是目前比较稳的版本,API 风格成熟,状态管理顺手。新项目直接上 5.x 就好,老项目还在用 4.x 的也不用强行迁移,因为核心用法差异不大。

网络权限这一步特别容易漏。Android 项目要在AndroidManifest.xml加INTERNET权限,鸿蒙项目需要在ohos/entry/src/main/module.json5里声明:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }

这个权限不加的话,dio 发请求会静默失败或者直接抛异常。关键是它在编译期不报错,只有运行到网络请求时才暴露。我第一次跑网络请求时就在这个点上卡了十几分钟,所以必须划出来提醒你。

注意:部分 DevEco 版本在module.json5变更后,已经安装到模拟器的旧包不会自动更新权限,建议改完权限后重新安装一次,或者手动卸载重装。

3.2 请求工具类 HttpUtil 封装

为了让页面代码保持干净,我把请求层封装成一个HttpUtil单例。这个方法我在 Android 和 iOS 项目里也一直用,在鸿蒙上同样适用:

import 'package:dio/dio.dart'; class HttpUtil { static final HttpUtil _instance = HttpUtil._internal(); factory HttpUtil() => _instance; late final Dio dio; HttpUtil._internal() { dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json; charset=utf-8', }, )); dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, error: true, )); dio.interceptors.add(InterceptorsWrapper( onError: (e, handler) { // 在这里做统一的错误提示,比如断网、超时 handler.next(e); }, )); } }

封装完之后,业务层拿数据就很干净:

final response = await HttpUtil().dio.get('/v1/games');

关于 dio 5.x 的泛型,我特别多说一句。dio 5 在反序列化时,如果你指定了List<dynamic>这种泛型,和 4.x 的处理方式有差异,最容易出现的问题就是拿到response.data后发现它是 Map 而不是 List。稳妥的做法是直接不指定泛型,或者用response.data as List<dynamic>在模型层做转换,这样反而更可控。

3.3 拦截器的正确分工

拦截器是 dio 最实用的设计,但很多人用不好。我的习惯是分工明确:LogInterceptor管日志,InterceptorsWrapper管业务错误。日志拦截器在 debug 模式才开,release 模式要关掉,不然流量和日志输出会成为性能负担,而且万一泄露了敏感请求体,线上出问题很尴尬。

业务错误拦截器里可以做的事很多,比如统一弹 Toast、统一把 401 错误转发到登录页、统一处理断网异常等。但要注意,拦截器里不要做过于重的逻辑,尤其是不要在里面直接调网络请求,否则容易形成递归调用,调试起来很痛苦。我见过一个新人在onError里写重试逻辑,结果因为条件判断写错,请求失败后无限重试,把测试服务器的日志刷爆了。

4. 游戏列表应用的核心实现

4.1 数据模型与 fromJson 的规范

今天示例接口返回的 JSON 结构大致是:

[ { "id": 1, "name": "Open World Demo", "rating": 4.6, "downloads": 12345, "iconUrl": "https://example.com/icons/1.png" } ]

对应的 Dart 模型:

class Game { final int id; final String name; final double rating; final int downloads; final String iconUrl; const Game({ required this.id, required this.name, required this.rating, required this.downloads, required this.iconUrl, }); factory Game.fromJson(Map<String, dynamic> json) { return Game( id: json['id'] as int, name: json['name'] as String, rating: (json['rating'] as num).toDouble(), downloads: json['downloads'] as int, iconUrl: json['iconUrl'] as String, ); } }

模型层的fromJson里,我习惯用显式强转而不是一把梭。尤其rating这个字段,后端返回的数字在 JSON 解析后可能是 int,也可能直接解析成 double,直接as double很容易炸,用(x as num).toDouble()才是稳妥写法。这个细节在 Dart 2.12 之后尤其重要,类型转换不严谨迟早会在线上遇到类型错误。

4.2 Provider 状态管理:ChangeNotifier 实战

游戏列表这种场景,最省事的是用 FutureBuilder 加载一次数据。但我这次把 provider 加进来,有两个原因:一是列表后续要加搜索、收藏,状态会越来越多;二是很多人问 Provider 怎么用,正好借这个案例展示一套标准写法。

GameListViewModel继承ChangeNotifier,负责管理数据加载和分页状态:

class GameListViewModel extends ChangeNotifier { final _http = HttpUtil().dio; List<Game> games = []; bool loading = false; int page = 1; bool hasMore = true; Future<void> loadGames({bool refresh = false}) async { if (refresh) { page = 1; hasMore = true; } if (loading || !hasMore) return; loading = true; notifyListeners(); try { final response = await _http.get('/v1/games', queryParameters: {'page': page, 'pageSize': 20}); final data = response.data as List<dynamic>; final list = data .map((e) => Game.fromJson(e as Map<String, dynamic>)) .toList(); if (refresh) { games = list; } else { games.addAll(list); } hasMore = list.length >= 20; page++; } catch (e) { // 这里交给全局拦截器或本地处理 } finally { loading = false; notifyListeners(); } } }

注意notifyListeners()的调用时机。我在loading改变后和games改变后都调用了它。如果漏掉一次,界面可能停留在旧状态,这是 Provider 使用中最容易踩的新手错误。

然后在应用入口注入:

void main() { runApp( ChangeNotifierProvider( create: (_) => GameListViewModel(), child: const GameListApp(), ), ); }

组件里用context.watch<GameListViewModel>()监听状态变化,用context.read<GameListViewModel>()触发一次性动作。这一套就是 Provider 解决组件通信的基本思路:共享同一份 ViewModel,数据自然就通了。很多人纠结组件通信,其实第一步不是去学各种通信框架,而是先把「数据放上面、UI 从下面读」这个模型玩熟。

4.3 列表 UI、下拉刷新与加载更多

页面主体用ListView.builder渲染游戏卡片。RefreshIndicator配合加载更多的实现是这样的:

Widget build(BuildContext context) { final viewModel = context.watch<GameListViewModel>(); return Scaffold( appBar: AppBar(title: const Text('游戏列表(OHOS)')), body: RefreshIndicator( onRefresh: () => viewModel.loadGames(refresh: true), child: ListView.builder( itemCount: viewModel.games.length + 1, itemBuilder: (context, index) { if (index == viewModel.games.length) { return viewModel.hasMore ? const Center(child: CircularProgressIndicator()) : const Center(child: Text('没有更多了')); } final game = viewModel.games[index]; return ListTile( leading: game.iconUrl.isNotEmpty ? Image.network(game.iconUrl, width: 48, height: 48, fit: BoxFit.cover) : null, title: Text(game.name), subtitle: Text('评分:${game.rating} 下载:${game.downloads}'), ); }, ), ), ); }

itemCount加 1 的目的是给列表尾巴留一个加载状态位,这样用户滑到底部能看到加载动画或“没有更多”的提示。加载更多的触发我写在ScrollController的监听里,滚动到底部时调用viewModel.loadGames()。

这里有个重要的性能注意点:不要在build方法里直接触发加载,也不要在列表项的 build 里写复杂的网络逻辑,否则每一次 setState 都会引发新的加载,最终导致无限 rebuild,页面会卡成幻灯片。正确的做法是把加载动作放在滚动监听的回调里,并且通过loading标志位避免重复触发。

4.4 网络图片加载与弱网兜底

Image.network在鸿蒙上默认走 Flutter 引擎内置的 HttpClient,所以同样受module.json5网络权限管控,权限没配好时图片会裂掉。除此之外,鸿蒙模拟器的图片加载速度不算快,最好是给图片加上loadingBuilder和errorBuilder:

Image.network( game.iconUrl, width: 48, height: 48, fit: BoxFit.cover, loadingBuilder: (context, child, progress) { if (progress == null) return child; return const SizedBox(width: 48, height: 48, child: Center(child: CircularProgressIndicator())); }, errorBuilder: (context, error, stackTrace) { return const SizedBox(width: 48, height: 48, child: Icon(Icons.broken_image)); }, )

如果你追求更好的性能,可以尝试把cached_network_image加进来做图片缓存。不过我实测发现cached_network_image在 OpenHarmony 端有时会依赖平台通道的插件,如果没有完整适配,加载会出问题。所以新手上路阶段先别急着上缓存库,用Image.network加 loading/error 兜底已经能覆盖绝大多数场景。

5. 实测中遇到的坑与排查方法

5.1 flutter 新建项目后跑不起来的排查路径

这个问题太常见了,几乎每个第一次接触 Flutter for OpenHarmony 的人都会遇到。现象是 DevEco 里打开 ohos 工程后编译报错,找不到 Flutter 运行时;或者flutter run提示 no devices,但 DevEco 侧明明能看到模拟器。

排查顺序我建议固定为三步。第一步,flutter doctor -v看 Flutter 通道和引擎分支是不是 ohos 适配版;第二步,flutter devices看是否能识别 ohos 设备,识别不到就看 OpenHarmony SDK 路径和环境变量配了没有;第三步,查 DevEco 里 Hvigor 版本和 Flutter 插件是否匹配。绝大多数跑不起来的问题,都出在第二步和第三步,而不是你的 Dart 代码本身。

还有一种情况是项目从别人那里 clone 下来,Flutter SDK 版本和本机不一致。我个人的习惯是在项目根目录放一个.fvmrc,用 FVM 锁定 Flutter SDK 版本,这样团队协作可以避免“我这边能跑你那边不行”的尴尬。这个习惯在鸿蒙适配链路上尤其重要,因为版本错位的排查成本比标准 Flutter 高得多。

5.2 dio 请求失败:超时、证书与异常日志

鸿蒙模拟器上跑 dio,第一个高频问题是超时:connectTimeout 设了 10 秒,但请求一直没有返回。常见原因有两个,一是模拟器 DNS 解析异常,二是请求的域名是 HTTPS,而鸿蒙上的证书校验比 Android 更严格。

HTTPS 证书问题在调试期最直接的解决办法是临时关闭校验:

(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.badCertificateCallback = (cert, host, port) => true; return client; };

但这只适合本地调试或私服环境,上线前一定要收回。我见过有人把这行代码直接留在生产项目里,结果安全测试一抓一个准,这种教训不值得再踩。

再回答一下热词里提到的e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhand...这类报错。这个日志头本身并不神秘,它只是 Flutter 引擎捕获到了一个未被处理的 Dart 异常。真正重要的是它下面跟的那段堆栈,绝大多数情况是 dio 的异步错误没有 catch 住,加一个onError拦截器或者 Future 的catchError就能解决。看到这种报错先别慌,往下看堆栈定位问题就好。

5.3 Provider 与组件通信在鸿蒙端的小坑

Provider 本身和 Flutter 引擎绑定很深,鸿蒙适配理论上不影响状态管理逻辑。我唯一遇到的问题是热重载:在 DevEco 里改动 Dart 代码后点热重载,Provider 的create偶尔不会重建,导致页面显示的还是旧数据。这个不是 Provider 的锅,而是 Flutter for OpenHarmony 的 hot reload 在部分场景下不够完整。解法很简单,遇到状态不对就先全量重启,别在热重载上死磕。

组件通信方面,除了 Provider,还有NotificationListener、InheritedWidget、StreamController等方案。你在网上搜组件通信,一定能看到大量技术贴,但我建议普通业务场景先学会 Provider 的context.watch和context.read就够了。它能覆盖 90% 的场景。等你真的碰到跨组件多层传递事件的复杂需求,再深入研究 Stream 和事件总线不迟。

5.4 常见问题速查表

问题可能原因解决方式
编译找不到 Flutter 运行时Flutter SDK 分支不是 ohos 适配版换用 flutter_flutter 的 ohos 分支
flutter devices看不到鸿蒙设备OpenHarmony SDK 路径未配置检查环境变量和 DevEco SDK 设置
dio 请求一直超时module.json5未声明网络权限添加ohos.permission.INTERNET并重装应用
HTTPS 请求报证书错误鸿蒙证书校验严格调试期临时关闭校验,上线前务必收回
图片加载失败或裂图网络权限未配置或图片地址协议问题检查权限 + 加errorBuilder兜底
热重载后数据没有更新鸿蒙热重载支持不完整全量热重启或手动重启应用
编译器报 main.dart 未找到DevEco 打开了整个 Flutter 项目而非 ohos 子目录只打开ohos/目录

6. 最后说点个人体会

DAY 3 做下来,最强烈的感受是:Flutter for OpenHarmony 并不是“另一个移动平台”,而更像一个长线工程。你会明显感觉到工具链成熟度和 Android 相比还有差距,但核心链路已经可以跑了。我能用 Dart 写完业务、用 dio 拉数据、用 Provider 管理状态,这在一年前是不太敢想的事。

实操中我建议你保持一个习惯:每天记录一个坑和它的解决方式。比如今天最值的一条经验就是“网络请求没通,先查 module.json5 的权限,再查证书,最后才去怀疑 dio 配置”。这类经验积累多了,后面再碰鸿蒙 Flutter 开发会顺利很多。

如果你打算照着这个项目练手,下一步可以往三个方向扩展:给游戏列表加搜索和分类筛选、引入图片缓存库完善列表流畅度、把列表换成无限滚动虚拟列表。每次扩展都在逼着你解决真实工程问题,这才是 DAY 3 之后真正有意义的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询