1. Day 3 开篇:在 OpenHarmony 上做网络请求,别照搬 Android 那套
把 Flutter 工程跑到 OpenHarmony 开发板上之后,我原本以为网络请求这块是最省心的。毕竟 Flutter 的 HTTP 层是纯 Dart 实现,底层走自己的 socket 封装,理论上跟宿主系统没有半毛钱关系。但 Day 3 真正把网络请求接进去、把数据清单列表刷出来之后,我才发现这里面的坑比 UI 适配要多得多。
这个系列记录的是我用 Flutter For OpenHarmony 做跨端应用的全过程。前两天把环境搭好、把默认工程跑起来之后,Day 3 的目标很明确:接入网络请求,从服务端拉一份数据清单,在页面上用列表展示出来。听起来是再常规不过的需求,但放到 OpenHarmony 上,"常规"两个字要打问号。
先说结论:Flutter 应用在 OpenHarmony 上做网络请求,整体思路和 Android 一致,但有几处关键差异必须处理,否则就是编译通过、运行直接崩,或者请求发出去了但数据回不来。这篇文章把 Day 3 的完整过程、踩过的坑、以及排查思路都捋一遍,希望对正在折腾 Flutter For OpenHarmony 的人有帮助。
1.1 OpenHarmony 上的 Flutter 运行环境:它到底是什么
很多人对"OpenHarmony 上跑 Flutter"的第一反应是"套了一个壳"。这个理解不够准确。OpenHarmony 是独立开源的操作系统,Flutter 能在上面跑,靠的是社区维护的 OpenHarmony 分支,核心是 flutter_flutter(Flutter 仓库的 OpenHarmony 适配),再加上配套的 flutter_engine 和 flutter_plugins 仓库。
这套适配的本质,是把 Flutter 引擎作为 OpenHarmony 的一个 native 模块编进去,UI 渲染走的是 Flutter 自己的 Skia,而不是系统自带的 ArkUI。Dart 层代码(dart:io、dart:convert、dart:async 这些标准库)绝大部分可以原样运行,但凡是需要跟系统能力打交道的 plugin,就必须有 OpenHarmony 的实现,否则 platform channel 调用会直接落空。
网络请求恰好夹在中间:Dart 层发请求没问题,但底层的 DNS 解析、socket 连接、证书校验,最终都要落到操作系统上。OpenHarmony 的网络栈跟 Android 不是一套,于是你会遇到一些"在 Android 上根本不会出现"的现象。比如我这次遇到的一个情况:请求超时时间明明设了 10 秒,但实际要等将近 20 秒才报错,后来检查下来是 OpenHarmony 侧的网络权限没声明,请求被系统拦了,Dart 层拿到的不是立即失败,而是长时间等待。这种表现非常误导人,我一开始以为是对端服务问题,白查了半天。
1.2 网络权限声明:module.json5 而不是 AndroidManifest.xml
在 Android 上做网络请求,第一件事是去 AndroidManifest.xml 加 <uses-permission android:name="android.permission.INTERNET"/>。在 OpenHarmony 上,对应的文件不是 AndroidManifest.xml,而是模块配置 module.json5。这个文件一般在工程 OpenHarmony 侧的 entry 模块下,目录结构类似:
code复制your_project/
├── ohos/
│ ├── entry/
│ │ └── src/main/module.json5
│ └── ...
找到 entry/src/main/module.json5,在 module 节点下的 requestPermissions 数组里加上 INTERNET 权限:
json5复制{
"module": {
"name": "entry",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_reason",
"usedScene": {
"abilities": ["EntryAbility"]
}
}
]
}
}
reason 和 usedScene 在 OpenHarmony 的权限声明里属于推荐交互信息,reason 指向 string 资源,usedScene 声明使用场景。调试阶段 reason 可以临时写一个字符串,但正式发布前必须补齐,否则应用市场审核会卡。如果你还要判断当前网络是否可用、检查网络状态,可以顺便加上 ohos.permission.GET_NETWORK_INFO。
这里有个值得记住的排查顺序:Flutter 页面里请求失败,先别急着改 Dart 代码,先确认 OpenHarmony 侧权限、module.json5 配置、以及应用是否真的安装到了目标设备上。权限没声明导致的失败,往往以超时或者 SocketException 的形式出现,跟代码问题混在一起非常难分辨。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 网络请求接入:Dio 为主、HttpClient 为辅的选型思路
网络层选型是我在 Day 3 开始前就纠结过的问题。Flutter 官方提供了 dart:io 的 HttpClient,社区最流行的是 dio,还有 http 包。三者都能用,但放在 OpenHarmony 场景下,考虑的东西会多一点。
我的最终选择是 Dio 作为主网络库,原因是它在纯 Dart 层完成大部分工作,不依赖 Android 或 iOS 的原生能力,在 OpenHarmony 上不需要额外做平台适配。下面把选型逻辑和具体接入代码展开说。
2.1 为什么我选了 Dio 而不是 dart:io 自带 HttpClient
先说 dart:io 的 HttpClient。它够底层,但用起来"裸"得厉害:没有拦截器机制,没有统一的错误处理,JSON 序列化要自己拼接,请求参数要手动拼到 URL 或者 body 里。写一个简单的 GET 没问题,一旦涉及统一的 token 头、日志打印、错误码解析,代码会迅速膨胀到没法维护。
http 包比 HttpClient 友好很多,API 简单,但同样缺少拦截器和取消机制,适合快速验证接口,不适合作为项目主网络层。
Dio 的好处是这些能力开箱即用:BaseOptions 统一管理 baseUrl、超时、请求头,拦截器可以做日志、鉴权、重试,还支持取消请求、FormData、文件下载进度。更关键的是,Dio 5.x 的核心逻辑全部由 Dart 实现,不需要任何原生代码参与。OpenHarmony 分支的 Flutter 能跑 Dart 层代码,就等于能跑 Dio。这一点比那些依赖 platform channel 的网络库靠谱得多。
2.2 BaseOptions、拦截器与超时配置的完整代码
我建了一个 ApiClient 单例来统一管理 Dio 实例,核心代码如下:
dart复制import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
late final Dio dio;
ApiClient._internal() {
dio = Dio(
BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
},
),
);
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (obj) => debugPrint(obj.toString()),
),
);
}
}
两点提醒。第一,Dio 5.x 的超时参数类型是 Duration,Dio 4.x 是毫秒整数。如果你参照网上老教程写 connectTimeout: 10000,编译直接报错。第二,LogInterceptor 在 release 模式记得去掉,否则响应体全文打印在生产环境属于安全隐患,也会拖慢性能。我习惯用 kReleaseMode 判断,只在 debug 模式下添加日志拦截器。
接口请求我封装在 Repository 层,没有直接在页面里散落 Dio 调用。这样页面只管 UI 状态,数据获取逻辑集中在一个文件里,后续换接口域名、加统一参数只改一处。
2.3 HTTPS 与证书校验:在 OpenHarmony 上的注意点
Flutter 在 Android 和 iOS 上默认使用自己的 BoringSSL 证书库,不直接走系统证书。OpenHarmony 分支的引擎在证书管理上会有些差异,我实际遇到的典型报错是:
code复制HandshakeException: Handshake error in client (OS Error:
CERTIFICATE_VERIFY_FAILED: certificate verify failed)
出现这种错误,先检查服务端证书链是否完整,特别是中间证书有没有配齐。很多 HTTPS 站点只部署了叶子证书,在浏览器里正常,但在 Flutter 的严格校验下就会握手失败。
如果只是调试环境用的自签名证书,可以临时关掉校验,但绝对不能带进生产代码:
dart复制dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
);
生产环境正确做法是用 SecurityContext 加载受信任的证书,或者做证书固定(Certificate Pinning)。OpenHarmony 上这个机制和 Flutter 标准版一致,因为证书校验逻辑在 Dart 层和 BoringSSL 里,宿主系统只提供底层 socket。理解这一点,排错的时候就不会被"是不是 OpenHarmony 证书库跟 Android 不一样"这种问题带偏。
3. 数据清单列表构建:从 JSON 到可滚动列表的完整链路
网络请求打通只是第一步,Day 3 的另一半是数据清单列表。我的做法是:接口返回 JSON,转成模型对象,再交给 FutureBuilder 管理加载状态,最后用 ListView.builder 渲染。链路上的每一步都有讲究,尤其是数据模型层,看似简单,但偷懒写出来的代码后面都会还债。
3.1 数据模型层:手写 fromJson 与自动生成怎么选
我这次接口返回的是文章列表,每条数据有 id、title、body 三个字段。模型层代码我选择了手写,因为字段少,结构固定:
dart复制class Article {
final int id;
final String title;
final String body;
const Article({
required this.id,
required this.title,
required this.body,
});
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'] as int? ?? 0,
title: json['title'] as String? ?? '',
body: json['body'] as String? ?? '',
);
}
}
注意字段都做了空安全兜底。服务端返回的数据不可控,某个字段缺失或者类型不对,直接 as int 会在运行期抛类型转换异常,整个列表就崩了。用 as int? ?? 0 这种写法,至少能让页面展示出来,不至于一个脏数据打挂全屏。
如果项目里模型很多(十几个以上),建议用 json_serializable 配合 build_runner 自动生成。但在 OpenHarmony 的 Flutter 工具链下,build_runner 版本和 Flutter 版本要严格匹配,我见过因为 json_serializable 版本过新导致 codegen 失败的情况。项目初期模型少,手写完全够用,等模型膨胀了再迁移也不迟。
3.2 FutureBuilder + ListView.builder 的组合方式
数据获取和 UI 关联的写法,我推荐一个简单可靠的组合:
先定义一个负责数据的 Repository 方法:
dart复制class ArticleRepository {
final ApiClient apiClient;
ArticleRepository(this.apiClient);
Future<List<Article>> fetchArticles() async {
final resp = await apiClient.dio.get<List<dynamic>>('/posts');
final data = resp.data ?? [];
return data
.map((e) => Article.fromJson(e as Map<String, dynamic>))
.toList();
}
}
页面侧用 FutureBuilder 管理状态:
dart复制FutureBuilder<List<Article>>(
future: _articlesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorRetryView(
message: snapshot.error.toString(),
onRetry: _reload,
);
}
final items = snapshot.data ?? [];
if (items.isEmpty) {
return const Center(child: Text('暂无数据'));
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ArticleListItem(article: items[index]);
},
);
},
)
_reload 方法重新创建 future,然后 setState 触发重建:
dart复制void _reload() {
setState(() {
_articlesFuture = _repository.fetchArticles();
});
}
这里有个容易被忽略的点:FutureBuilder 的 future 如果在 build 方法里直接创建,每次重建都会触发新的请求。正确做法是把它存成 State 里的成员变量,只有主动刷新时才重新赋值。这个坑对所有 Flutter 新手都适用,在 OpenHarmony 上表现更明显,因为热重载时容易重复触发网络请求。
3.3 下拉刷新、加载更多与空状态处理
列表页只做一次加载肯定不行。我用 RefreshIndicator 做下拉刷新,数据量大了之后还要做分页加载。
下拉刷新代码:
dart复制RefreshIndicator(
onRefresh: () async {
_reload();
await _articlesFuture;
},
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) => ArticleListItem(...),
),
)
AlwaysScrollableScrollPhysics 是必须的,否则列表内容不满一屏时,下拉刷新手势不生效,这个细节很多人会踩。
加载更多我用的 ScrollController 监听到底部:
dart复制_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
});
分页加载要注意防重入:正在加载更多时用户继续滑动,监听器会触发多次请求,导致数据重复。我在 Repository 里加了一个 _isLoadingMore 标志位,请求中直接 return。另一个常见问题是分页数据源返回空时,应该把"没有更多了"的状态记录下来,避免每次滑到底部都发一次无效请求。
空状态和错误状态我用单独的 Widget 展示,不直接在 build 里塞几个三目运算符。这样代码可读性好,后续加图片、加按钮都好维护。
4. 真机联调与抓包:hdc、Charles 与 RK 系列开发板
Day 3 的重头戏在联调环节。模拟器上功能正常不代表真机上没问题,尤其是网络权限、证书校验、系统代理这些,模拟器和真机行为差异很大。我自己用的测试设备是 RK3568 开发板,后面又借了一台 RK3588,两者都是 OpenHarmony 社区常用的硬件平台。
4.1 hdc 基础操作:连接设备、查看系统版本、安装应用
hdc 是 OpenHarmony 的官方设备连接调试工具,作用和 adb 类似,但命令不通用。Day 3 里我用的最多的几条命令:
bash复制# 查看已连接的设备
hdc list targets
# 查看系统版本信息
hdc shell param get const.product.name
hdc shell param get const.product.model
hdc shell param get const.ohos.version
# 安装应用
hdc install entry-default-signed.hap
# 向设备发送文件
hdc file send ./local.json /data/local/tmp/local.json
连接 RK3568 这类开发板时,如果是网络连接,需要先用 USB 连一次,然后用 hdc tconn IP:端口 建立网络通道。开发板和电脑在同一局域网时,用网络通道调试比 USB 方便得多,不用一直挂着线。
为什么我要强调 param get 这套命令?因为 Flutter For OpenHarmony 的适配版本跟系统版本强相关。你用的 flutter_flutter 分支可能只兼容特定版本的 OpenHarmony API,系统版本太新或者太旧,引擎跑起来都可能出问题。拿到一台新设备,第一件事就是查清系统版本,再决定用哪个分支编译,这个习惯能让后面少很多诡异问题。
4.2 用 Charles 抓 Flutter 请求的正确姿势
Charles 是调试 HTTP/HTTPS 请求的常用工具,但在 Flutter 场景下有个大坑:Flutter 的 dart:io 网络栈默认不走系统代理。你在 OpenHarmony 的系统设置里配好代理,Chrome、系统浏览器都会走,唯独 Flutter 应用不走,因为它的 socket 连接是自己建立的,不读系统代理配置。
所以要让 Charles 抓到 Flutter 发出的请求,正确做法是在 Dart 代码里显式设置代理。Dio 5.x 可以通过自定义 HttpClientAdapter 实现:
dart复制import 'package:dio/io.dart';
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.findProxy = (uri) {
return 'PROXY 192.168.1.100:8888';
};
return client;
},
);
192.168.1.100 换成你电脑的局域网 IP,8888 是 Charles 默认的 HTTP 代理端口。配好之后重启应用,Charles 里就能看到 Flutter 的请求了。注意这种代理配置只用于调试,提交代码前一定要去掉,否则真机用户会全部走你的电脑代理,直接完蛋。
抓 HTTPS 请求时,还要在 Charles 里启用 SSL Proxying,并安装 Charles 的根证书。OpenHarmony 上装根证书比 Android 麻烦,需要把证书文件 push 到设备,然后在系统设置里手动安装。如果只是调试用,更快的替代方案是临时在代码里加 badCertificateCallback,不过我记得这个开关只能用于本地测试环境。
4.3 Chrome 抓不到请求?代理设置这一步最容易被忽略
调试过程中我还遇到一个插曲:用 flutter run -d chrome 跑 Flutter Web 版本做对比时,Chrome 里发的请求 Charles 抓不到。排查了一圈,最后发现是 Chrome 的"安全 DNS"功能导致的问题。Chrome 默认开启了 DNS over HTTPS(DoH),请求直接走 DoH 加密通道,绕过了系统代理,Charles 自然什么都看不到。
解决办法是在 Chrome 的设置里关闭安全 DNS,或者在 Charles 的 SSL Proxying 配置里把目标域名加进列表。还有一个常见原因是抓包工具的根证书没装到系统信任区,HTTPS 握手阶段就被掐断了,表现也是"抓不到请求"。
这个问题的价值和前面的 Flutter 不走系统代理是一个道理:排查"抓不到包"问题,先弄清楚流量到底走哪条链路。是系统代理被忽略了,还是 DoH 绕过了代理,还是证书不被信任,逐个排除,比重新装一遍工具效率高得多。
5. Day 3 实测踩坑记录:六个典型问题与排查链路
以下问题是我 Day 3 实际遇到、并且花时间排查过的。每个问题我都按"现象 → 根因 → 处理方式"来记录,方便你对照自己的情况。
5.1 Flutter main Gradle plugin 报错:插件应用方式不对
构建 OpenHarmony 侧工程时,我遇到一个编译报错,关键字是:
code复制You are applying Flutter's main Gradle plugin imperatively using the apply script
method, which is not supported...
这个报错的意思是 settings.gradle 里用了旧的脚本来加载 Flutter Gradle 插件。新版 Flutter 要求改用 plugin management 方式:
gradle复制plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
}
替换掉原来 apply from: "$flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle" 这种写法。这个问题的根源是 Flutter 工具链升级后,旧的 Gradle 集成方式被废弃了。很多从老版本工程升级上来的项目,都会在这一步卡住。
处理方式不复杂:打开 ohos 侧工程的 settings.gradle,把插件加载方式改成 plugins block,然后清理 Gradle 缓存重编译。注意如果同时保留了 Android 侧配置,两边都要检查,因为 Flutter 3.16 之后对 Gradle 插件应用方式管得很严。
5.2 MediaCodecVideoRenderer 渲染异常:分清主次,别陷进去
开发过程中,日志里频繁出现一个渲染层错误,关键词是 MediaCodecVideoRenderer。这是播放视频时常见的渲染器错误,通常跟硬件解码、视频源格式有关。我的列表页里恰好有个视频缩略图预览功能,现象是:列表滑动时概率性黑屏,控制台刷一堆错误日志。
排查下来,问题出在视频预览插件走了平台通道,在 OpenHarmony 上没有对应的 MediaCodec 实现,于是抛出了 Android 框架的错误。这类插件即使编译通过,运行时也大概率出问题。
处理方式是换掉依赖平台通道的预览方案,改用纯 Dart 生成的缩略图,或者直接调用 OpenHarmony 兼容的插件。这个案例很好地说明了:在 OpenHarmony 上排查问题,先确认报错来自 Dart 层还是原生层。原生层报错,往往意味着某个 plugin 不可用,而不是你的业务代码有问题。
5.3 下载文件到私有目录与权限申请的关系
表格里我有一列数据需要缓存到本地,下载到应用私有目录时,我以为需要申请存储权限。实际上,Flutter 里用 path_provider 获取应用私有目录,再往里面写文件,不需要任何存储权限。Android 和 OpenHarmony 对应用私有目录都是放开的,只有写到公共存储(比如系统下载目录、SD 卡根目录)才需要申请存储权限。
dart复制final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/cache.json');
await file.writeAsString(jsonData);
如果你在 OpenHarmony 上遇到 "Permission denied",先确认是不是用了 getExternalStorageDirectory 这类公共目录 API。能放私有目录就别放公共目录,这个原则不仅省权限申请,也符合系统安全模型,用户卸载应用时数据也会跟着清理干净。
5.4 热重载后页面没更新:先热重启,再想代码问题
开发时我还踩过一个开发工具层面的坑:改了页面代码,按热重载,模拟器上页面没变化,一度怀疑是 OpenHarmony 分支的热重载机制有问题。后来发现是热重载本身在某些状态(比如改了数据模型、改了 pubspec 依赖)下不会生效,需要按大写 R 热重启,甚至完全重新编译。
我的建议是:在 OpenHarmony 分支下开发,养成"依赖改动 → 重启应用;纯 UI 改动 → 热重载"的习惯。热重载没反应不代表写错了,先热重启验证一次,避免在错误方向上浪费时间。如果是 Flutter Web 调试,热重载后浏览器没更新,多半是编译产物没刷新,在终端执行一次强制刷新的构建命令通常能解决。
5.5 插件兼容性:OpenHarmony 不是所有 pub.dev 插件都能用
这个坑我反复踩,必须单独拎出来说。pub.dev 上大量插件是 Android 和 iOS 平台通道实现,OpenHarmony 分支的 Flutter 默认没有这些原生实现,直接 add 依赖后编译能过,运行到调用处就崩,报错通常是 "MissingPluginException" 或者干脆卡死。
选插件前先确认两点:一是插件是否是纯 Dart 实现(像 dio、cached_network_image 的核心依赖 flutter_cache_manager 基本是纯 Dart),二是社区 flutter_plugins 仓库是否提供了 OpenHarmony 实现。像 path_provider 这类基础插件,现在有 OpenHarmony 版本可用,但版本号跟官方可能不一致,需要从对应仓库拉。
我给自己定了个规矩:每引入一个新插件,先在干净的 OpenHarmony 环境跑一遍最小示例,确认能用再集成到项目里。这一步虽然麻烦,但能省掉后续大量"运行时崩溃却查不到原因"的时间。
6. 经历过 Day 3,我想给后续几天的自己留几句话
网络层和列表页只是整个应用的地基部分,但地基打不牢,后面做缓存、做离线、做推送都会出问题。这一天的实践,让我对 Flutter For OpenHarmony 有了几个明确判断。
第一,网络层必须隔离。Dio 只是当前选择,OpenHarmony 的 Flutter 分支还在演进,将来如果引擎层对 dart:io 行为有调整,只要网络调用都封装在 Repository 层,迁移成本就低。绝不要在页面里散落 URL 和 Dio 实例。
第二,权限、证书、代理这三件事,是 OpenHarmony 上网络调试的三大拦路虎。任何请求异常,按"权限声明 → 证书校验 → 代理链路"的顺序排查,基本能覆盖大部分问题。我这次就是先栽在权限上,又栽在代理上,绕了两圈才总结出这个套路。
第三,跑通功能只是起点,还要在 RK3568 和 RK3588 两类设备上都验证一遍。不同芯片的网络行为、渲染行为都有细微差别,特别是弱网环境下的表现,开发板模拟不出真实用户手机网络波动的情况。
后面的计划是给列表加上缓存层,断网时也能展示历史数据,然后补一个请求重试机制,处理弱网下的临时失败。这两个功能做完,这个数据清单列表才真正算得上"可用"而不只是"能跑"。到时候再回来分享实测数据。
