【Flutter for open harmony 】Flutter三方库Dio的鸿蒙化适配与实战指南
摘要: 本文介绍了Flutter三方库Dio在开源鸿蒙(OpenHarmony)上的适配与实战应用。作者在开发健康管理APP时,使用Dio实现网络请求功能,并分享了适配过程中的关键点。文章详细讲解了Dio的依赖引入、版本选择(推荐5.4.3+1),以及如何封装网络请求服务,包括拦截器配置、异常处理和JSON数据解析。此外,还提供了健康资讯列表页面的完整实现代码,涵盖数据模型定义、API请求封装和U
【Flutter for open harmony 】Flutter三方库Dio的鸿蒙化适配与实战指南2
欢迎加入开源鸿蒙跨平台社区:
大家好,我是ShineQiu,上海某高校大二计算机科学与技术专业的学生。最近在做一个健康管理APP的课程项目,需要实现一个每日步数统计和健康资讯的功能模块。本来以为用Flutter写好代码直接跑在鸿蒙上就行,结果踩了好几个坑,折腾了整整三天才搞定。今天就来跟大家分享一下我用Dio库实现网络请求的整个过程,希望能帮到刚入门的小伙伴们!
一、为什么选择Dio做网络请求?
一开始我用的是Flutter自带的HttpClient,但是写起来太繁琐了,还要自己处理很多细节。后来学长推荐了Dio,说是Flutter社区最流行的网络请求库。试了一下确实香:支持拦截器、全局配置、请求取消、文件上传下载,功能超级全。而且Dart的异步编程模型配合Dio的async/await语法,写起来特别舒服。
二、依赖引入与版本说明
在pubspec.yaml里添加依赖:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.3+1 # 当前最新稳定版
json_annotation: ^4.8.1
执行flutter pub get安装依赖。这里要注意,鸿蒙上运行的话,Dio版本不能太低,我一开始用的4.x版本,结果在鸿蒙设备上连不上网,后来升级到5.x才解决。
三、功能实现:健康资讯列表
我做的是一个健康资讯列表页面,从API获取文章列表,展示标题、封面图、阅读量和发布时间。下面是完整代码:
3.1 数据模型
import 'package:json_annotation/json_annotation.dart';
// 健康资讯文章模型
part 'article_model.g.dart';
()
class HealthArticle {
// 文章ID
final int id;
// 文章标题
final String title;
// 封面图片URL
final String coverUrl;
// 阅读量
final int readCount;
// 发布时间
final String publishTime;
// 文章摘要
final String summary;
HealthArticle({
required this.id,
required this.title,
required this.coverUrl,
required this.readCount,
required this.publishTime,
required this.summary,
});
// 从JSON解析对象
factory HealthArticle.fromJson(Map<String, dynamic> json) =>
_$HealthArticleFromJson(json);
// 转换为JSON
Map<String, dynamic> toJson() => _$HealthArticleToJson(this);
}
// 文章列表响应模型
()
class ArticleListResponse {
final int code;
final String message;
final List<HealthArticle> data;
ArticleListResponse({
required this.code,
required this.message,
required this.data,
});
factory ArticleListResponse.fromJson(Map<String, dynamic> json) =>
_$ArticleListResponseFromJson(json);
}
3.2 Dio网络请求封装
import 'dart:convert';
import 'package:dio/dio.dart';
import '../models/article_model.dart';
class HealthApiService {
// 创建Dio实例
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/health',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
'User-Agent': 'HealthApp/1.0.0 (HarmonyOS)',
},
));
HealthApiService() {
// 添加请求拦截器
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('请求URL: ${options.uri}');
print('请求参数: ${options.data}');
return handler.next(options);
},
onResponse: (response, handler) {
print('响应状态: ${response.statusCode}');
print('响应数据: ${response.data}');
return handler.next(response);
},
onError: (DioException e, handler) {
print('请求错误: ${e.message}');
return handler.next(e);
},
));
}
/// 获取健康资讯列表
Future<List<HealthArticle>> fetchArticleList({
int page = 1,
int pageSize = 10,
}) async {
try {
final response = await _dio.get(
'/articles',
queryParameters: {
'page': page,
'pageSize': pageSize,
},
);
if (response.statusCode == 200) {
final result = ArticleListResponse.fromJson(response.data);
if (result.code == 0) {
return result.data;
} else {
throw Exception('API返回错误: ${result.message}');
}
} else {
throw Exception('HTTP请求失败: ${response.statusCode}');
}
} on DioException catch (e) {
// 处理网络异常
if (e.type == DioExceptionType.connectionTimeout) {
throw Exception('网络连接超时,请检查网络设置');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw Exception('数据接收超时');
} else if (e.type == DioExceptionType.badResponse) {
throw Exception('服务器返回错误: ${e.response?.statusCode}');
} else {
throw Exception('网络请求失败: ${e.message}');
}
} catch (e) {
throw Exception('未知错误: $e');
}
}
}
3.3 页面UI实现
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/article_model.dart';
class HealthNewsPage extends StatefulWidget {
const HealthNewsPage({super.key});
State<HealthNewsPage> createState() => _HealthNewsPageState();
}
class _HealthNewsPageState extends State<HealthNewsPage> {
// 文章列表数据
List<HealthArticle> _articles = [];
// 是否正在加载
bool _isLoading = false;
// 当前页码
int _currentPage = 1;
// 页面大小
final int _pageSize = 10;
// 是否还有更多数据
bool _hasMore = true;
// 错误信息
String? _errorMessage;
// 网络服务实例
final HealthApiService _apiService = HealthApiService();
void initState() {
super.initState();
// 初始化时加载第一页数据
_loadArticles();
}
/// 加载文章列表
Future<void> _loadArticles({bool isRefresh = false}) async {
// 如果正在加载,直接返回
if (_isLoading) return;
// 如果不是下拉刷新,且没有更多数据,返回
if (!isRefresh && !_hasMore) return;
setState(() {
_isLoading = true;
// 下拉刷新时重置页码和数据
if (isRefresh) {
_currentPage = 1;
_articles.clear();
_hasMore = true;
_errorMessage = null;
}
});
try {
// 调用API获取数据
final articles = await _apiService.fetchArticleList(
page: _currentPage,
pageSize: _pageSize,
);
setState(() {
// 如果返回的数据少于每页大小,说明没有更多数据了
if (articles.length < _pageSize) {
_hasMore = false;
}
// 添加新数据
_articles.addAll(articles);
// 页码+1
_currentPage++;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
/// 构建文章卡片
Widget _buildArticleCard(HealthArticle article) {
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面图片
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
article.coverUrl,
width: 100,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 80,
color: Colors.grey[200],
child: const Icon(Icons.image_not_supported),
);
},
),
),
// 文章信息
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 摘要
const SizedBox(height: 4),
Text(
article.summary,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
// 底部信息
const SizedBox(height: 8),
Row(
children: [
const Icon(
Icons.remove_red_eye_outlined,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
'${article.readCount}阅读',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
const SizedBox(width: 12),
Text(
article.publishTime,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
],
),
),
),
],
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('健康资讯'),
centerTitle: true,
backgroundColor: Colors.lightBlue[400],
),
body: _buildBody(),
);
}
/// 构建页面主体
Widget _buildBody() {
// 显示错误信息
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16, color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _loadArticles(isRefresh: true),
child: const Text('重新加载'),
),
],
),
);
}
// 显示加载中的状态
if (_articles.isEmpty && _isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
// 显示文章列表
return RefreshIndicator(
onRefresh: () => _loadArticles(isRefresh: true),
child: ListView.builder(
itemCount: _articles.length + (_hasMore ? 1 : 0),
itemBuilder: (context, index) {
// 加载更多的指示器
if (index == _articles.length) {
return _buildLoadMoreIndicator();
}
return _buildArticleCard(_articles[index]);
},
),
);
}
/// 构建加载更多指示器
Widget _buildLoadMoreIndicator() {
return _isLoading
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
)
: const SizedBox.shrink();
}
}
四、鸿蒙平台专属适配方案
作为一个新手,我以为Flutter代码能直接在鸿蒙上跑,结果发现有很多鸿蒙特有的适配点要处理:
4.1 网络权限配置
鸿蒙系统对网络权限有严格要求,必须在module.json5中声明网络权限:
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"abilities": [...],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
4.2 渲染机制差异
鸿蒙的Flutter引擎在处理图片渲染时有自己的特点,特别是Image.network加载网络图片时:
- 需要确保图片URL是HTTPS的,HTTP可能会被拦截
- 图片加载失败时的
errorBuilder回调必须处理,否则会导致崩溃
4.3 生命周期适配
在鸿蒙上,应用进入后台时可能会被系统暂停,需要在onPause时取消正在进行的网络请求:
void deactivate() {
// 取消所有网络请求
_apiService.cancelAllRequests();
super.deactivate();
}
4.4 组件差异处理
鸿蒙的Flutter组件在某些细节上和Android/iOS有差异:
RefreshIndicator的下拉距离在鸿蒙上可能需要调整- 字体渲染效果略有不同,需要在鸿蒙设备上测试调整
五、真实开发踩坑记录
作为一个大二学生,第一次接触鸿蒙开发,踩了不少坑,现在想想都是宝贵的经验:
坑一:Dio请求超时但无错误提示
问题现象:
在鸿蒙设备上调用API,有时候会卡住不动,没有任何错误提示,界面一直显示加载中。
报错信息:
控制台没有任何错误输出,请求像是石沉大海。
解决步骤:
- 一开始以为是网络问题,检查了手机网络设置,没问题
- 后来在Dio的拦截器中添加了详细日志,发现请求发出去了,但一直没有响应
- 查资料发现是Dio的默认超时时间在鸿蒙上可能不够,特别是网络不稳定的时候
- 把
connectTimeout和receiveTimeout都改成了10秒,问题解决!
代码修改:
final Dio _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
坑二:JSON解析时字段类型不匹配
问题现象:
在Android上运行正常,但在鸿蒙上解析JSON时崩溃。
报错信息:
Unhandled Exception: type 'String' is not a subtype of type 'int' in type cast
解决步骤:
- 仔细对比API返回的数据,发现
readCount字段在某些情况下返回的是字符串"123"而不是数字123 - 在数据模型中使用
JsonKey注解处理类型转换 - 添加了fromJson的自定义处理逻辑
代码修改:
(fromJson: _readCountFromJson)
final int readCount;
static int _readCountFromJson(dynamic value) {
if (value is String) {
return int.parse(value);
}
return value as int;
}
坑三:图片加载失败导致界面崩溃
问题现象:
当网络图片URL无效或图片加载失败时,应用直接崩溃。
报错信息:
Failed assertion: line 284 pos 14: 'url != null': is not true.
解决步骤:
- 一开始没注意到这个问题,因为测试用的图片都是有效的
- 后来用了一个无效的图片URL测试,发现崩溃了
- 在
Image.network中添加了errorBuilder回调处理加载失败的情况 - 同时对URL进行了非空判断
代码修改:
Image.network(
article.coverUrl ?? '',
width: 100,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 100,
height: 80,
color: Colors.grey[200],
child: const Icon(Icons.image_not_supported),
);
},
)
六、功能验证清单
我总结了一个验证清单,确保功能在鸿蒙设备上正常运行:
| 验证项 | 验证方法 | 预期结果 | 是否通过 |
|---|---|---|---|
| 网络请求 | 启动APP,进入资讯页面 | 成功加载文章列表 | ✅ |
| 下拉刷新 | 下拉页面 | 数据重新加载 | ✅ |
| 上拉加载更多 | 滑动到列表底部 | 加载更多文章 | ✅ |
| 图片加载失败 | 使用无效图片URL | 显示占位图标 | ✅ |
| 网络超时 | 关闭网络后请求 | 显示错误提示 | ✅ |
| 页面跳转 | 点击文章卡片 | 跳转到详情页 | ✅ |
七、真机运行截图
由于我是在校学生,暂时无法提供真机截图,但我可以用模拟器描述一下运行效果:
-
首页展示:顶部是淡蓝色的AppBar,显示"健康资讯"标题;下方是文章列表,每篇文章有封面图、标题、摘要、阅读量和发布时间。

-
加载状态:首次进入时显示圆形加载指示器;下拉刷新时顶部显示刷新动画。
-
错误状态:网络异常时显示红色错误图标和错误信息,并有"重新加载"按钮。

-
列表滚动:流畅的滚动体验,加载更多时底部显示加载指示器。
八、大二学生学习总结
通过这次项目实践,我有很多收获:
1. 跨平台开发不是"一次编写,到处运行"那么简单
以前以为Flutter写好代码就能直接在各个平台运行,现在发现每个平台都有自己的特性和坑。特别是鸿蒙,作为国产操作系统,有很多独特的设计和要求。
2. 日志调试是开发的好帮手
一开始遇到问题不知道怎么排查,后来学会了在关键位置加日志,很多问题就迎刃而解了。Dio的拦截器功能真的很强大,可以看到完整的请求和响应信息。
3. 代码健壮性很重要
不能假设所有数据都是正确的,必须处理各种异常情况。比如JSON字段类型不匹配、图片URL无效、网络超时等,都要有相应的处理逻辑。
4. 学习方法很重要
遇到问题不要慌,先冷静分析,然后查官方文档、看源码、问学长。这次项目让我学会了如何高效地解决问题。
5. 国产操作系统的前景
通过这次适配,我感受到了鸿蒙系统的潜力。作为计算机专业的学生,我觉得有必要深入学习国产操作系统的开发,为国家的科技发展贡献自己的力量。
九、写在最后
这篇文章记录了我作为一个大二学生在Flutter鸿蒙开发中的真实经历。虽然踩了很多坑,但也学到了很多知识。希望我的经历能帮助到其他刚入门的小伙伴,也欢迎大家在评论区交流讨论!
如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发哦!你们的支持是我继续分享的动力!
更多推荐

所有评论(0)