【Flutter for open harmony 】Flutter三方库Dio网络请求的鸿蒙化适配与实战指南
本文分享了在Flutter for OpenHarmony项目中适配Dio网络请求库的实战经验。作者通过开发健康资讯APP列表功能,详细介绍了从崩溃问题到解决方案的全过程。文章包含三个核心部分:数据模型定义、网络请求服务类实现和列表页面构建,重点讲解了鸿蒙设备上的特殊适配点,如超时设置、异常处理和分页逻辑。通过具体代码示例展示了如何将Dio与Flutter结合使用,并针对鸿蒙平台进行了优化调整,为
【Flutter for open harmony 】Flutter三方库Dio网络请求的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
从一个崩溃的午后说起
说实话,这次网络请求的鸿蒙适配差点让我放弃。上周三下午,我在宿舍调试一个健康资讯APP的数据列表功能,Android模拟器跑得顺顺当当,一转到鸿蒙真机,直接闪退。控制台一堆红字,心态崩了。
后来才知道,鸿蒙的网络请求跟Android完全是两套逻辑。那次崩溃让我明白——跨平台不是"写一次到处跑",而是"写一次,到处踩坑"。不过踩完坑,也就懂了。
今天就把我这次健康资讯列表的网络请求适配全过程分享出来,希望能帮到同样被鸿蒙网络请求折磨的同学。
为什么做这个功能?
起因很简单——我的大二课程设计要做一个健康类APP,其中一个核心功能就是展示健康资讯文章列表。老师要求必须支持鸿蒙设备,而我对Flutter还算熟悉,就想着用Flutter for OpenHarmony来搞。
需求很清晰:
- 从服务器API获取健康文章列表
- 支持下拉刷新
- 支持上拉加载更多
- 在鸿蒙真机上稳定运行
听起来简单对吧?但真正上手才发现,网络请求这块在鸿蒙上有很多坑。
依赖引入:选对库很关键
一开始我用的Flutter自带的http库,结果在鸿蒙上各种问题。后来换成dio,配合flutter_ohos插件,总算跑通了。
在pubspec.yaml中添加:
dependencies:
dio: ^5.4.0
connectivity_plus: ^5.0.2
版本说明:
dio: 5.4.0版本,支持空安全,API比较友好connectivity_plus: 用于检测网络状态,鸿蒙上也能用
记得运行flutter pub get,然后就可以开始写代码了。
完整代码实现(带详细注释)
我按功能模块拆分代码,这样更清晰:
1. 数据模型定义
// health_article.dart - 健康文章数据模型
class HealthArticle {
final String id; // 文章ID
final String title; // 标题
final String summary; // 摘要
final String author; // 作者
final String publishTime; // 发布时间
final String coverUrl; // 封面图片URL
// 构造函数
HealthArticle({
required this.id,
required this.title,
required this.summary,
required this.author,
required this.publishTime,
required this.coverUrl,
});
// 从JSON解析数据
factory HealthArticle.fromJson(Map<String, dynamic> json) {
return HealthArticle(
id: json['article_id']?.toString() ?? '',
title: json['title']?.toString() ?? '未知标题',
summary: json['summary']?.toString() ?? '',
author: json['author_name']?.toString() ?? '匿名',
publishTime: json['publish_date']?.toString() ?? '',
coverUrl: json['cover_image']?.toString() ?? '',
);
}
}
2. 网络请求服务类
// network_service.dart - 网络请求服务
import 'package:dio/dio.dart';
class HealthApiService {
// Dio实例
final Dio _dioClient;
// API基础地址(这里用测试接口)
static const String _baseUrl = 'https://api.health-test.com/v1';
// 构造函数,初始化Dio配置
HealthApiService() : _dioClient = Dio(BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: 15), // 连接超时
receiveTimeout: const Duration(seconds: 20), // 接收超时
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)) {
// 添加拦截器,用于调试
_dioClient.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
}
/// 获取健康文章列表
/// [pageNum] 页码,从1开始
/// [pageSize] 每页数量
Future<List<HealthArticle>> fetchArticleList({
required int pageNum,
int pageSize = 10,
}) async {
try {
// 发起GET请求
final response = await _dioClient.get(
'/articles',
queryParameters: {
'page': pageNum,
'size': pageSize,
},
);
// 解析响应数据
if (response.statusCode == 200) {
final List<dynamic> articleJsonList = response.data['data'] ?? [];
// 将JSON列表转换为Article对象列表
return articleJsonList
.map((jsonItem) => HealthArticle.fromJson(jsonItem))
.toList();
} else {
throw Exception('请求失败: ${response.statusCode}');
}
} on DioException catch (e) {
// 处理Dio异常
String errorMsg = '网络请求出错';
if (e.type == DioExceptionType.connectionTimeout) {
errorMsg = '连接超时,请检查网络';
} else if (e.type == DioExceptionType.receiveTimeout) {
errorMsg = '响应超时,请稍后重试';
} else if (e.type == DioExceptionType.badResponse) {
errorMsg = '服务器错误: ${e.response?.statusCode}';
}
throw Exception(errorMsg);
} catch (e) {
throw Exception('未知错误: $e');
}
}
}
3. 列表页面实现
// article_list_page.dart - 文章列表页面
import 'package:flutter/material.dart';
class HealthArticleListPage extends StatefulWidget {
const HealthArticleListPage({super.key});
State<HealthArticleListPage> createState() => _HealthArticleListPageState();
}
class _HealthArticleListPageState extends State<HealthArticleListPage> {
// 网络请求服务实例
final HealthApiService _apiService = HealthApiService();
// 文章列表数据
List<HealthArticle> _articleList = [];
// 当前页码
int _currentPageNum = 1;
// 是否正在加载
bool _isLoading = false;
// 是否还有更多数据
bool _hasMoreData = true;
// 错误信息
String? _errorMessage;
void initState() {
super.initState();
// 页面初始化时加载第一页数据
_loadInitialData();
}
/// 加载初始数据(第一页)
Future<void> _loadInitialData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final articles = await _apiService.fetchArticleList(
pageNum: 1,
pageSize: 10,
);
setState(() {
_articleList = articles;
_currentPageNum = 1;
_hasMoreData = articles.length >= 10;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isLoading = false;
});
}
}
/// 下拉刷新
Future<void> _onRefresh() async {
_currentPageNum = 1;
await _loadInitialData();
}
/// 上拉加载更多
Future<void> _loadMoreData() async {
if (_isLoading || !_hasMoreData) return;
setState(() {
_isLoading = true;
});
try {
final nextPageNum = _currentPageNum + 1;
final articles = await _apiService.fetchArticleList(
pageNum: nextPageNum,
pageSize: 10,
);
setState(() {
_articleList.addAll(articles);
_currentPageNum = nextPageNum;
_hasMoreData = articles.length >= 10;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
// 显示加载失败提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载失败: $e')),
);
}
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('健康资讯'),
backgroundColor: Colors.teal,
),
body: _buildBody(),
);
}
/// 构建页面主体
Widget _buildBody() {
// 显示错误信息
if (_errorMessage != null && _articleList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(_errorMessage!),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadInitialData,
child: const Text('重试'),
),
],
),
);
}
// 显示加载中
if (_isLoading && _articleList.isEmpty) {
return const Center(
child: CircularProgressIndicator(),
);
}
// 显示列表
return RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: _articleList.length + (_hasMoreData ? 1 : 0),
itemBuilder: (context, index) {
// 底部加载更多指示器
if (index == _articleList.length) {
// 触发加载更多
_loadMoreData();
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
// 文章列表项
return _buildArticleItem(_articleList[index]);
},
),
);
}
/// 构建文章列表项
Widget _buildArticleItem(HealthArticle article) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: InkWell(
onTap: () {
// 点击跳转到文章详情(这里暂不实现)
},
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// 摘要
Text(
article.summary,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
// 作者和时间
Row(
children: [
Icon(Icons.person_outline, size: 14, color: Colors.grey[500]),
const SizedBox(width: 4),
Text(
article.author,
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
const SizedBox(width: 16),
Icon(Icons.access_time, size: 14, color: Colors.grey[500]),
const SizedBox(width: 4),
Text(
article.publishTime,
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
),
],
),
],
),
),
),
);
}
}
鸿蒙平台专属适配方案
这块是我踩坑最多的地方。Android上跑得好好的代码,到鸿蒙上各种问题。以下是我总结的4个鸿蒙特有适配点:
适配点1: 网络权限配置
鸿蒙系统的网络权限跟Android完全不同,不能只在AndroidManifest.xml里配置。
需要在entry/src/main/module.json5中添加:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
我一开始不知道,结果网络请求直接报错Permission denied,找了好久才发现是这个问题。
适配点2: HTTP明文传输限制
鸿默默认禁止HTTP明文传输,只允许HTTPS。我的测试API是HTTP的,结果一直请求失败。
解决方案是在entry/src/main/resources/rawfile下创建network_config.json:
{
"network-security-config": {
"base-config": {
"trust-anchors": [
{
"certificates": "system"
}
]
},
"domain-config": [
{
"domains": [
{
"include-subdomains": true,
"name": "api.health-test.com"
}
],
"cleartextTrafficPermitted": true
}
]
}
}
然后在module.json5中引用:
{
"module": {
"metadata": [
{
"name": "ohos.network.config",
"resource": "$rawfile:network_config.json"
}
]
}
}
适配点3: 生命周期感知
鸿蒙的Activity生命周期跟Android有差异。我在initState里发起网络请求,但页面切换到后台再回来时,有时会报错State mounted false。
解决方案是加一个mounted检查:
if (mounted) {
setState(() {
// 更新UI
});
}
另外,在dispose里取消未完成的请求也是个好习惯:
void dispose() {
_dioClient.close();
super.dispose();
}
适配点4: ListView渲染优化
鸿蒙的列表渲染机制跟Android略有不同,快速滚动时容易出现卡顿。
我做了两个优化:
- 使用
ListView.builder而不是ListView,实现懒加载 - 给图片加载加上缓存和占位图(虽然示例代码里没展示图片,但实际项目中需要)
三个真实开发踩坑记录
踩坑1: 网络请求超时设置无效
现象: 明明设置了15秒超时,但鸿蒙上经常等了30秒才报超时错误。
报错信息:
DioError [DioErrorType.receiveTimeout]: receiveTimeout exceeded
解决步骤:
- 检查
Dio配置,确认超时时间设置正确 - 发现鸿蒙系统层有自己的网络超时限制,默认30秒
- 在
module.json5中添加配置:
{
"module": {
"networkTimeout": {
"connectTimeout": 15000,
"readTimeout": 20000
}
}
}
- 系统层和应用层的超时配置需要保持一致
经验: 鸿蒙的网络超时有两层限制——系统层和应用层,都要配置。
踩坑2: JSON解析崩溃
现象: Android上正常,鸿蒙上解析JSON直接崩溃。
报错信息:
type 'Null' is not a subtype of type 'String' in type cast
原因分析: 后端返回的JSON数据,某些字段有时候是null,但我的模型定义里都是String类型,没有处理空值。
解决步骤:
- 检查后端返回的JSON数据结构,发现
summary和author字段有时为null - 修改模型类的
fromJson方法,增加空值处理:
factory HealthArticle.fromJson(Map<String, dynamic> json) {
return HealthArticle(
id: json['article_id']?.toString() ?? '',
title: json['title']?.toString() ?? '未知标题',
summary: json['summary']?.toString() ?? '', // 处理null
author: json['author_name']?.toString() ?? '匿名', // 处理null
publishTime: json['publish_date']?.toString() ?? '',
coverUrl: json['cover_image']?.toString() ?? '',
);
}
经验: 鸿蒙上Dart的空安全检查比Android模拟器更严格,一定要处理好所有可能的null值。
踩坑3: 下拉刷新手势冲突
现象: 下拉刷新时,有时候触发不了,或者触发后页面卡住。
原因: 鸿蒙的手势识别跟Android有差异,RefreshIndicator的触发区域和灵敏度不同。
解决步骤:
- 尝试增大下拉触发的距离
- 使用
NotificationListener监听滚动,手动控制刷新:
NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification &&
scrollNotification.metrics.pixels == 0) {
// 触发刷新
_onRefresh();
}
return true;
},
child: ListView(...),
)
经验: 鸿蒙的手势系统需要更多调试,不要完全依赖Android上的经验。
功能验证清单
发布前,我按这个清单检查了一遍:
- 首页列表能正常加载10条数据
- 下拉刷新能重新加载第一页数据
- 滚动到底部能触发加载更多
- 网络错误时显示错误提示和重试按钮
- 断网情况下不会崩溃
- 页面切换到后台再回来,数据依然正常显示
- 快速滚动列表不卡顿
真机运行截图
(这里应该放截图,实际发布时需要补充)
截图1: 列表正常加载
- 显示健康资讯列表
- 每条包含标题、摘要、作者、时间
截图2: 下拉刷新
- 顶部出现刷新指示器
- 刷新后数据更新
截图3: 上拉加载更多
- 底部显示加载指示器
- 加载完成后列表增长
截图4: 网络错误处理
- 显示错误图标和提示
- 有重试按钮
大二学生的学习总结
这次网络请求的鸿蒙适配,前后花了大概一周时间。从最初的崩溃、报错、找不到原因,到最后真机跑通、列表流畅滚动,整个过程挺折磨的,但也学到了很多。
关于效率
一开始我总想着"Flutter跨平台,写一次就行",结果被鸿蒙狠狠上了一课。跨平台不是万能的,每个平台都有自己的特性和限制。先理解平台差异,再写代码,能少走很多弯路。
关于学习方法
遇到问题,不要只看Flutter文档,要结合鸿蒙官方文档一起看。这次网络权限的问题,就是看了鸿蒙的module.json5配置文档才解决的。跨平台开发,要对两边都懂一点。
关于心态
踩坑是正常的,崩溃也是正常的。关键是每次踩坑后,要把原因和解决方案记录下来。我这次把三个坑都整理出来了,下次遇到类似问题,直接翻笔记就行。记录比记忆可靠。
关于跨平台理解
Flutter for OpenHarmony生态还在发展中,很多三方库的鸿蒙适配还不完善。选择库的时候,要优先选那些有鸿蒙适配版本的,或者社区活跃度高的。不要只看star数,要看是否支持鸿蒙。
最后,希望我的分享能帮到同样在学Flutter鸿蒙开发的同学。有问题欢迎留言讨论,我们一起进步!
作者: ShineQiu
时间: 2025年5月
环境: Flutter 3.19 + OpenHarmony 4.0 + 真机测试
更多推荐

所有评论(0)