ARTICLE DETAIL

资讯详情

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

Flutter鸿蒙开发实战:集成dio网络库打造游戏列表应用

Flutter鸿蒙开发实战:集成dio网络库打造游戏列表应用 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 SDKflutter_flutter 仓库的 ohos 分支版本选 3.x 系列JDKDevEco Studio 自带的 JDK 17版本选择上有个原则OpenHarmony SDK 和 flutter_flutter 分支需要保持节奏同步不要拿 Flutter 官方主分支去跑 ohos 平台。因为 ohos 平台适配是在独立分支上进行的主分支的新特性未必同步过来。我见过有人直接 clone 官方 Flutter SDK然后在命令行里加--platformsohos结果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 --platformsohos .第二行命令会往已有项目里补上 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 和 providerdependencies: flutter: sdk: flutter dio: ^5.7.0 provider: ^6.1.2dio 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; charsetutf-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 在反序列化时如果你指定了Listdynamic这种泛型和 4.x 的处理方式有差异最容易出现的问题就是拿到response.data后发现它是 Map 而不是 List。稳妥的做法是直接不指定泛型或者用response.data as Listdynamic在模型层做转换这样反而更可控。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(MapString, 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; ListGame games []; bool loading false; int page 1; bool hasMore true; Futurevoid 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 Listdynamic; final list data .map((e) Game.fromJson(e as MapString, 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.watchGameListViewModel()监听状态变化用context.readGameListViewModel()触发一次性动作。这一套就是 Provider 解决组件通信的基本思路共享同一份 ViewModel数据自然就通了。很多人纠结组件通信其实第一步不是去学各种通信框架而是先把「数据放上面、UI 从下面读」这个模型玩熟。4.3 列表 UI、下拉刷新与加载更多页面主体用ListView.builder渲染游戏卡片。RefreshIndicator配合加载更多的实现是这样的Widget build(BuildContext context) { final viewModel context.watchGameListViewModel(); 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和errorBuilderImage.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 之后真正有意义的部分。
返回列表