Flutter 三方库 dio 的鸿蒙化适配指南:打造企业级网络请求层

一、引言:为什么选 dio 作为适配示例
在 Flutter 生态中,dio 是毫无争议的 HTTP 客户端一哥。它支持拦截器、FormData、文件上传下载、请求取消、Mock 测试、超时控制等企业级特性,国内绝大多数 Flutter 团队的网络层都选它。
dio 不是纯 Dart 包,它的底层调用了 dart:io 中的原生 HTTP 能力。这意味着 Flutter 官方只内置了 Android 和 iOS 两个平台的 Embedder 实现,OpenHarmony 平台需要额外适配。
好消息是:Flutter 鸿蒙社区(CPF-Flutter)已经完成了这一层适配工作,并且适配方案对上层 API 做到了完全兼容——你的现有 dio 代码几乎不需要任何修改,在鸿蒙上就能直接运行。
本文目标:以 dio v5.4.3 为例,完整演示:
- 如何在 Flutter 鸿蒙项目中正确集成 dio
- 单例封装 + 拦截器体系(AuthInterceptor、LogInterceptor)
- 文件上传(
onSendProgress进度回调) - OpenHarmony 平台的 5 个特殊注意事项
- 完整生产级网络模块代码示例
二、环境准备:项目创建与依赖配置
2.1 环境要求
| 依赖项 | 版本要求 | 说明 |
|---|---|---|
| Flutter SDK | ≥ 3.35.7(推荐 3.44.9) | 包含 OpenHarmony 平台支持 |
| OpenHarmony SDK | 7.0.0(API 26) | 鸿蒙平台运行依赖 |
| dio | ≥ 5.4.0(推荐 5.4.3) | 社区已完成鸿蒙适配 |
| Dart | ≥ 3.4.0 |
2.2 创建支持鸿蒙的 Flutter 项目
# 检查 Flutter 版本
flutter --version
# Flutter 3.44.9 • channel stable
# 创建项目并指定支持的平台
flutter create --platforms=openharmony dio\_ohos\_demo
cd dio\_ohos\_demo
# 添加 dio 依赖
flutter pub add dio
# 查看 dio 版本
flutter pub deps | grep dio
# Should output: dio 5.4.3
2.3 pubspec.yaml 完整配置
name: dio\_ohos\_demo
description: Flutter dio 鸿蒙化适配演示项目
environment:
sdk: '>=3.4.0 <4.0.0'
flutter: '>=3.44.0'
dependencies:
flutter:
sdk: flutter
# ★ 核心:HTTP 客户端(社区已适配 OpenHarmony)
dio: ^5.4.3+1
# 以下库由社区完成鸿蒙适配,可直接使用
connectivity\_plus: ^6.0.5 # 网络状态监听
path\_provider: ^2.1.4 # 文件路径获取
flutter\_inappwebview: ^6.1.5+2 # WebView(部分功能已适配)
dev\_dependencies:
flutter\_test:
sdk: flutter
flutter\_lints: ^4.0.0
flutter:
uses-material-design: true
2.4 验证 dio 在鸿蒙平台是否可用
运行以下命令,确认 Flutter doctor 报告 OpenHarmony 平台正常:
flutter doctor
# ✓ OpenHarmony toolchain: Ready
# ✓ Flutter (OpenHarmony): Ready
—

三、核心功能验证:dio API 逐个测
3.1 单例封装:全局 Dio 客户端
在真实项目中,永远不要每次请求都 new Dio()。正确做法是封装一个全局单例客户端,集中管理 BaseURL、超时时间、拦截器等配置:
// lib/net/dio\_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
/// 鸿蒙化适配的 Dio 客户端单例
/// 配置与标准 Flutter dio 完全一致,社区适配层自动处理 OpenHarmony 差异
class DioClient {
static DioClient? \_instance;
late final Dio \_dio;
DioClient.\_internal() {
\_dio = Dio(
BaseOptions(
// API 基础地址(示例)
baseUrl: 'https://api.example.com',
// 连接超时:10 秒(网络不好时尽早报错)
connectTimeout: const Duration(seconds: 10),
// 接收超时:15 秒(等待服务器响应)
receiveTimeout: const Duration(seconds: 15),
// 发送超时:10 秒(上传文件时限制)
sendTimeout: const Duration(seconds: 10),
// 默认请求头
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// ── 拦截器 1:日志拦截器(仅 Debug 模式生效)──────────────
\_dio.interceptors.add(
LogInterceptor(
requestBody: true, // 打印请求体
responseBody: true, // 打印响应体
error: true, // 打印错误
logPrint: (obj) {
// 统一通过 debugPrint 输出,生产环境不打印
if (kDebugMode) {
debugPrint('\[Dio] $obj');
}
},
),
);
// ── 拦截器 2:Token 自动刷新拦截器(见第 4 节)────────────
\_dio.interceptors.add(AuthInterceptor());
}
static DioClient get instance {
\_instance ??= DioClient.\_internal();
return \_instance!;
}
Dio get client => \_dio;
}
**关键设计点**:全局单例保证所有请求共享同一个连接池(ConnectionPool),减少 TCP 握手开销。同时所有配置集中在一处,修改 BaseURL 或超时时间只需改一处代码。
3.2 GET 请求:带参数、错误处理、统一封装
// lib/net/article\_repository.dart
/// 文章列表实体
class Article {
final String id;
final String title;
final String summary;
final String author;
final DateTime publishedAt;
Article({
required this.id,
required this.title,
required this.summary,
required this.author,
required this.publishedAt,
});
factory Article.fromJson(Map<String, dynamic> json) => Article(
id: json\['id'] as String,
title: json\['title'] as String,
summary: json\['summary'] as String,
author: json\['author'] as String,
publishedAt: DateTime.parse(json\['publishedAt'] as String),
);
}
/// 获取文章列表
Future<List<Article>> fetchArticles({
int page = 1,
int pageSize = 20,
}) async {
try {
final response = await DioClient.instance.client.get(
'/articles',
queryParameters: {
'page': page,
'pageSize': pageSize,
},
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
// 业务错误码判断(如后端返回 { code: 0, data: \[...] })
if (data\['code'] == 0) {
final list = data\['data']\['list'] as List;
return list
.map((item) => Article.fromJson(item as Map<String, dynamic>))
.toList();
}
// 业务错误:抛出业务异常
throw DioException(
requestOptions: response.requestOptions,
message: data\['msg'] ?? '请求失败',
type: DioExceptionType.badResponse,
);
}
// HTTP 非 200
throw DioException(
requestOptions: response.requestOptions,
message: 'HTTP ${response.statusCode}',
type: DioExceptionType.badResponse,
);
} on DioException catch (e) {
// 统一错误转换:将底层异常转换为业务可理解的异常
throw \_transformDioError(e);
}
}
/// 统一错误转换:将 DioException 转换为业务异常 AppException
AppException \_transformDioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return AppException('网络连接超时,请检查网络设置');
case DioExceptionType.connectionError:
return AppException('网络连接失败,请确认网络畅通');
case DioExceptionType.cancel:
return AppException('请求已取消');
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
if (statusCode == 401) {
return AppException('登录已过期,请重新登录', code: 401);
} else if (statusCode == 403) {
return AppException('无访问权限');
} else if (statusCode == 404) {
return AppException('请求资源不存在');
} else if (statusCode != null \&\& statusCode >= 500) {
return AppException('服务器异常,请稍后重试');
}
return AppException('请求失败 ($statusCode)');
default:
return AppException('未知错误:${e.message}');
}
}
—
3.3 POST 请求:JSON Body 与 FormData
/// 用户登录
Future<LoginResult> login({
required String username,
required String password,
}) async {
try {
final response = await DioClient.instance.client.post(
'/auth/login',
data: {
'username': username,
'password': password, // 生产环境记得先做 MD5/SHA256 哈希
},
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
if (data\['code'] == 0) {
return LoginResult.fromJson(data\['data'] as Map<String, dynamic>);
}
throw AppException(data\['msg'] ?? '登录失败');
}
throw AppException('HTTP ${response.statusCode}');
} on DioException catch (e) {
throw \_transformDioError(e);
}
}
—
四、文件上传实战:带进度回调的上传功能
文件上传是网络层的常见需求,dio 提供了 onSendProgress 回调来实时获取上传进度。以下是完整实现:
4.1 基础文件上传
/// 上传图片文件到服务器(带进度回调)
Future<String> uploadImage(File imageFile) async {
// 1. 构建 FormData
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
imageFile.path,
// ⚠️ filename 建议使用英文,中文文件名在某些服务端可能导致乱码
filename: imageFile.path.split('/').last,
),
'type': 'avatar', // 额外参数:文件用途标识
});
try {
// 2. 发起请求,通过 onSendProgress 获取上传进度
final response = await DioClient.instance.client.post(
'/upload',
data: formData,
onSendProgress: (int sent, int total) {
// sent: 已发送字节数, total: 总字节数
final progress = (sent / total \* 100).toStringAsFixed(1);
debugPrint('\[上传进度] $progress% ($sent / $total)');
// 进度回调通常需要通知 UI 层(如更新进度条)
// 这里可以通过 Provider/Riverpod/StateNotifier 推送
// UploadProgressNotifier.update(progress);
},
);
// 3. 解析响应
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
if (data\['code'] == 0) {
return data\['data']\['url'] as String; // 返回上传后的文件 URL
}
throw AppException(data\['msg'] ?? '文件上传失败');
}
throw AppException('上传失败: HTTP ${response.statusCode}');
} on DioException catch (e) {
throw \_transformDioError(e);
}
}
4.2 UI 端:完整上传页面实现
以下是一个可以直接嵌入项目的 Flutter 上传页面,包含进度条、拦截器日志面板和底部技术栈标签:



五、AuthInterceptor:Token 自动刷新拦截器
企业级应用中,用户登录后拿到的 AccessToken 通常有有效期(常见 1~2 小时)。Token 过期后,服务器返回 401,传统的做法是跳转登录页让用户重新登录,体验极差。
正确做法是:在 401 发生时,自动用 RefreshToken 获取新 AccessToken,再重试原请求。这个逻辑统一封装在 AuthInterceptor 中,对业务代码完全透明。
5.1 拦截器实现
// lib/net/auth\_interceptor.dart
import 'dart:async';
import 'package:dio/dio.dart';
/// AuthInterceptor:自动处理 Token 刷新
///
/// 工作流程:
/// 1. 请求返回 401(Token 过期)
/// 2. 用 RefreshToken 向 /auth/refresh 发请求,换取新 AccessToken
/// 3. 将新 Token 写入本地存储
/// 4. 用新 Token 重试原请求
/// 5. 若 RefreshToken 也过期,清除登录态,通知上层跳转登录页
///
/// 注意:多个请求同时收到 401 时,只有一个去刷新 Token,其余排队等待
class AuthInterceptor extends Interceptor {
static const \_refreshTokenKey = 'refresh\_token';
static const \_accessTokenKey = 'access\_token';
bool \_isRefreshing = false; // 防止并发刷新
final List<\_QueuedRequest> \_pendingRequests = \[]; // 等待 Token 的请求队列
// ── 请求拦截:注入 AccessToken ───────────────────────────
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// 从本地存储读取 Token
final accessToken = await \_getStoredToken(\_accessTokenKey);
if (accessToken != null) {
options.headers\['Authorization'] = 'Bearer $accessToken';
}
handler.next(options);
}
// ── 错误拦截:处理 401 ──────────────────────────────────
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 仅拦截 401 错误,其他错误透传
if (err.response?.statusCode != 401) {
return handler.next(err);
}
// ── 场景:多个请求同时 401 ───────────────────────────────
// 只让第一个请求触发 Token 刷新,其余请求排队
if (\_isRefreshing) {
final completer = Completer<Response<dynamic>>();
\_pendingRequests.add(\_QueuedRequest(err.requestOptions, completer));
try {
final result = await completer.future;
handler.resolve(result);
} catch (e) {
handler.next(err);
}
return;
}
\_isRefreshing = true;
try {
// ── 步骤 1:用 RefreshToken 换取新 AccessToken ─────────
final refreshToken = await \_getStoredToken(\_refreshTokenKey);
if (refreshToken == null) {
throw Exception('RefreshToken 不存在,请重新登录');
}
// ★ 创建独立的 Dio 实例(禁止复用主实例,防止死锁!)
final refreshDio = Dio();
final refreshResp = await refreshDio.post(
'${DioClient.instance.client.options.baseUrl}/auth/refresh',
data: {'refreshToken': refreshToken},
);
final newAccessToken = refreshResp.data\['data']\['accessToken'] as String;
final newRefreshToken = refreshResp.data\['data']\['refreshToken'] as String;
// ── 步骤 2:保存新 Token ────────────────────────────────
await \_saveToken(\_accessTokenKey, newAccessToken);
await \_saveToken(\_refreshTokenKey, newRefreshToken);
// ── 步骤 3:重试排队的所有请求 ─────────────────────────
for (final req in \_pendingRequests) {
req.requestOptions.headers\['Authorization'] = 'Bearer $newAccessToken';
try {
final result = await DioClient.instance.client.fetch(
req.requestOptions,
);
req.completer.resolve(result);
} catch (e) {
req.completer.reject(e);
}
}
\_pendingRequests.clear();
// ── 步骤 4:重试当前 401 的请求 ────────────────────────
err.requestOptions.headers\['Authorization'] = 'Bearer $newAccessToken';
final result = await DioClient.instance.client.fetch(err.requestOptions);
\_isRefreshing = false;
handler.resolve(result);
} catch (e) {
// ── RefreshToken 也过期:清除登录态 ───────────────────
await \_clearAllTokens();
\_pendingRequests.clear();
\_isRefreshing = false;
// 通知上层(通过 AppException 的 code=401 区分)
handler.next(DioException(
requestOptions: err.requestOptions,
message: '登录已过期,请重新登录',
type: DioExceptionType.badResponse,
response: err.response?.copyWith(statusCode: 401),
));
}
}
// ── Token 存储(示例,实际建议用 flutter\_secure\_storage)──
Future<String?> \_getStoredToken(String key) async {
// 简化实现:使用 shared\_preferences
// 建议生产环境使用 flutter\_secure\_storage(敏感 Token 加密存储)
// import 'package:shared\_preferences/shared\_preferences.dart';
// final sp = await SharedPreferences.getInstance();
// return sp.getString(key);
return null; // placeholder
}
Future<void> \_saveToken(String key, String value) async {
// final sp = await SharedPreferences.getInstance();
// await sp.setString(key, value);
}
Future<void> \_clearAllTokens() async {
// final sp = await SharedPreferences.getInstance();
// await sp.remove(\_accessTokenKey);
// await sp.remove(\_refreshTokenKey);
}
}
class \_QueuedRequest {
final RequestOptions requestOptions;
final Completer<Response<dynamic>> completer;
\_QueuedRequest(this.requestOptions, this.completer);
}
5.2 业务层如何使用
业务代码完全不感知 Token 刷新过程,像正常请求一样写即可:
// 用户资料页
Future<UserProfile> fetchUserProfile() async {
final resp = await DioClient.instance.client.get('/user/profile');
return UserProfile.fromJson(resp.data\['data']);
}
// 如果 Token 过期,上面的代码会自动完成刷新+重试
// 业务层不需要任何额外处理
—
六、OpenHarmony 平台特殊注意事项
6.1 HTTPS 证书验证
OpenHarmony 默认严格校验服务器证书链,与 Android/iOS 行为有差异。开发阶段如遇 certificate verify failed,可在 onHttpClientCreate 中临时覆盖:
\_dio.httpClientAdapter = HttpClientAdapter();
/// 注意:仅开发环境使用,生产环境必须配置有效 CA 证书
/// 参考:OpenHarmony 支持的自定义 TrustManager 配置
6.2 中文文件名上传
// ❌ 中文文件名可能导致服务端收到乱码
'file': await MultipartFile.fromFile(path, filename: '我的图片.jpg')
// ✅ 方案一:URL 编码
'file': await MultipartFile.fromFile(
path,
filename: Uri.encodeComponent('我的图片.jpg'),
)
// ✅ 方案二:上传前重命名为 UUID,图片内容不变
'file': await MultipartFile.fromFile(
path,
filename: '${Uuid().v4()}.jpg', // 生成随机文件名
)
6.3 超时时间配置差异
| 超时类型 | 设置建议 | 说明 |
|---|---|---|
| connectTimeout | 10s | 网络不好时尽早报错 |
| receiveTimeout | 15s | 普通 API 响应 |
| sendTimeout | 30s | 上传大文件时放宽 |
| 下载大文件 | 单独设 CancelToken + UI 层兜底 | receiveTimeout 不能控制下载总时间 |
6.4 拦截器死锁问题
⚠️ **最常见陷阱**:在拦截器中调用
await DioClient.instance.client.get(...)刷新 Token 时,如果复用了同一个 Dio 实例,会造成死锁——请求在等 Token,Token 刷新请求也在等连接池。
解法:永远为 Token 刷新请求创建独立的 Dio 实例(参见上方 refreshDio 的用法)。
6.5 请求取消的 UI 联动
用户快速切换 Tab 时,之前 pending 的请求需要主动取消:
// 页面 State 中持有 CancelToken
class \_ArticleListState extends State<ArticleList> {
final CancelToken \_cancelToken = CancelToken();
void dispose() {
// 页面销毁时取消所有 pending 请求
\_cancelToken.cancel('页面已销毁');
super.dispose();
}
Future<void> loadArticles() async {
await DioClient.instance.client.get(
'/articles',
cancelToken: \_cancelToken, // 传入 CancelToken
);
}
}
七、Flutter 鸿蒙适配的核心原理
Flutter 的跨平台能力来自三层架构:
Flutter SDK(Framework)
↓ Dart 代码
Dart Runtime(Engine)
↓ 平台无关 API
Platform Embedder(Android / iOS / OpenHarmony)
↓ 平台实现
原生系统能力(HTTP、文件、传感器等)
dio 的 dart:io HTTP 调用在 Android/iOS 平台由 Flutter SDK 自带的 Embedder 处理。OpenHarmony 平台的适配工作,由 Flutter 鸿蒙社区(CPF-Flutter)完成——他们提供了 OpenHarmony 平台的 Embedder 实现,将 dart:io 的网络调用映射到 OpenHarmony 的 HTTP API。
对 Flutter 开发者而言:这一层完全透明。dio 的 API 写法在 Android、iOS、OpenHarmony 三个平台上完全一致。
八、避坑清单总结
| # | 坑描述 | 症状 | 解法 |
|---|---|---|---|
| 1 | 安装了标准版 dio(非 Flutter 渠道包) | 编译失败 | 使用 flutter pub add dio 而非直接 dart pub add |
| 2 | HTTPS 证书验证失败 | certificate verify failed | 开发阶段临时覆盖 TrustManager,生产必须配 CA |
| 3 | 中文文件名上传乱码 | 后台收到 ???? | URL 编码或改用 UUID 文件名 |
| 4 | 响应超时不生效(下载场景) | 请求一直等待 | receiveTimeout 只控制响应头超时,下载需另设 CancelToken 兜底 |
| 5 | 拦截器中复用主 Dio 实例 | 死锁,请求永远 pending | Token 刷新必须用独立的 new Dio() 实例 |
| 6 | 页面销毁时未取消请求 | 页面销毁后回调更新已卸载的 Widget | 页面 dispose 中调用 cancelToken.cancel() |
总结
- 零额外适配:Flutter 鸿蒙社区已完成 dio 的 OpenHarmony 桥接,现有代码直接可用,无需修改。
- 单例封装是基础:全局一个 Dio 实例,集中管理配置和拦截器,是企业级网络层的标准架构。
- 拦截器是灵魂:AuthInterceptor 自动处理 Token 刷新,让业务代码完全不用关心登录态过期。
onSendProgress是上传体验的保障:实时反馈上传进度,用户知道"还有多久",体验大幅提升。- OpenHarmony 差异需关注:证书验证、中文文件名、超时配置、拦截器死锁、请求取消。
以上
——这 5 个点我觉得是在鸿蒙上跑稳 dio 的关键!
更多推荐



所有评论(0)