【maaath】Flutter 三方库 dio 的鸿蒙化适配指南:跨平台工程下的网络请求统一封装
*** HTTP 请求配置选项*/method?header?extraData?: number;: number;/*** 统一响应结果包装器*//** 业务状态码,非 HTTP 状态码 *//** 响应消息 *//** 响应数据体 *//** HTTP 原始状态码 *//** HTTP 响应头 *//** 请求耗时(毫秒) *//*** HttpResult 静态工厂方法*/code: 0,
Flutter 三方库 dio 的鸿蒙化适配指南:跨平台工程下的网络请求统一封装
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
作者:maaath
一、引言
在移动应用开发中,网络请求几乎是所有业务场景的基础能力。当项目需要同时运行在 Android、iOS、Web 以及 OpenHarmony(鸿蒙)平台时,如何在 Flutter 跨平台工程中优雅地复用网络层代码,是每个开发者都会面临的核心问题。
Flutter 生态中,dio 是最流行的 HTTP 客户端库,提供了拦截器、统一响应处理、请求取消、超时控制等开箱即用的能力。然而,Flutter 标准 SDK 默认基于 dart:io,其中的 IOHttpClientAdapter 在 OpenHarmony 平台上存在兼容性问题——dart:io 的网络 API 无法直接映射到鸿蒙原生的网络接口。因此,我们需要对 dio 进行鸿蒙化适配,通过自定义 HttpClientAdapter 桥接 Flutter 端与 OpenHarmony 原生网络能力。
本文将以一个实际运行在鸿蒙设备上的 Flutter for OpenHarmony 跨平台工程为示例,详细讲解网络请求封装的设计思路与实现细节,并提供可直接运行的参考代码。
前置说明:本文假设读者已有 Flutter 基础,熟悉 Dart 语言,了解 OpenHarmony 应用的基本工程结构(尤其是 ETS/ArkTS 原生模块与 Dart 层的协同工作方式)。由于
flutter_flutter已在内部集成 hvigor 构建系统,Flutter 模块可以直接复用 OpenHarmony 原生网络 SDK(@ohos.net.http)的能力,这也是我们实现 dio 鸿蒙适配的核心思路。
二、总体设计
在 Flutter 跨平台工程的架构设计中,网络层承担着承上启下的角色:对上,向业务层暴露简洁易用的接口(GET/POST/PUT/DELETE);对下,根据不同平台选择最合适的网络实现:
- Android/iOS/Web 平台:直接使用 dio 默认的
IOHttpClientAdapter或BrowserHttpClientAdapter - OpenHarmony 平台:通过 MethodChannel 将 Dart 端的网络请求转发给 ETS/ArkTS 原生模块,由原生模块调用
@ohos.net.http完成实际通信
这一设计的优势在于:业务层代码无需感知平台差异,无论运行在哪个平台,调用方式完全一致。业务层只看到 httpService.get() 或 httpService.post() 这样的统一接口,而底层的平台适配逻辑被封装在适配层内部。
以下为整体架构分层:
┌─────────────────────────────────────┐
│ 业务层 (Dart / Flutter UI) │
│ httpService.get() / httpService.post() │
├─────────────────────────────────────┤
│ 网络服务层 (Dart) │
│ HttpService(统一封装,平台无关) │
├─────────────────────────────────────┤
│ 平台适配层 │
│ MethodChannel ──> OpenHarmony 原生 │
│ (@ohos.net.http + ETS HttpService) │
└─────────────────────────────────────┘
三、OpenHarmony 原生网络模块实现
考虑到部分读者可能没有现成的 ETS 网络模块,我们先给出 OpenHarmony 原生网络服务的完整实现。这个原生模块将作为 MethodChannel 的服务端,接收 Dart 端传来的请求参数,调用系统原生 API 发起网络请求,并将结果回传给 Dart 层。
3.1 数据模型定义(HttpModel.ets)
import http from '@ohos.net.http';
/**
* HTTP 请求配置选项
*/
export interface HttpRequestOptions {
url: string;
method?: http.RequestMethod;
header?: Record<string, string>;
extraData?: string | Object | ArrayBuffer;
connectTimeout?: number;
readTimeout?: number;
expectDataType?: http.HttpDataType;
}
/**
* 统一响应结果包装器
*/
export interface HttpResult {
/** 业务状态码,非 HTTP 状态码 */
code: number;
/** 响应消息 */
msg: string;
/** 响应数据体 */
data: Object | null;
/** HTTP 原始状态码 */
httpCode: number;
/** HTTP 响应头 */
headers: Record<string, string>;
/** 请求耗时(毫秒) */
costTime: number;
}
/**
* HttpResult 静态工厂方法
*/
export class ResponseFactory {
static success(
data: Object | null,
httpCode: number,
headers: Record<string, string>,
costTime: number,
msg: string
): HttpResult {
const result: HttpResult = {
code: 0,
msg: msg,
data: data,
httpCode: httpCode,
headers: headers,
costTime: costTime
};
return result;
}
static businessError(code: number, msg: string, costTime: number): HttpResult {
const result: HttpResult = {
code: code,
msg: msg,
data: null,
httpCode: 200,
headers: {},
costTime: costTime
};
return result;
}
static httpError(code: number, msg: string, httpCode: number, costTime: number): HttpResult {
const result: HttpResult = {
code: code,
msg: msg,
data: null,
httpCode: httpCode,
headers: {},
costTime: costTime
};
return result;
}
static timeout(): HttpResult {
return {
code: -2,
msg: "Request timeout",
data: null,
httpCode: -1,
headers: {},
costTime: -1
};
}
static cancelled(): HttpResult {
return {
code: -3,
msg: "Request cancelled",
data: null,
httpCode: -1,
headers: {},
costTime: -1
};
}
}
/**
* 错误码常量类
*/
export class ErrorCode {
static readonly SUCCESS: number = 0;
static readonly NETWORK_ERROR: number = -1;
static readonly TIMEOUT: number = -2;
static readonly CANCELLED: number = -3;
static readonly INVALID_PARAM: number = -4;
static readonly PARSE_ERROR: number = -5;
static readonly SERVER_ERROR: number = -6;
}
设计说明:将响应结果统一包装为
HttpResult是网络层设计的关键。我们的响应结构将业务状态码(code)与 HTTP 原始状态码(httpCode)分离处理:HTTP 2xx 代表网络层正常,具体业务逻辑是否成功由code(0 为成功)决定。这种设计使得业务层代码可以统一判断if (result.code === 0)来处理成功分支,无需关心底层 HTTP 细节。
3.2 核心网络服务实现(HttpService.ets)
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
import { logger } from '../util/Logger';
import {
HttpRequestOptions,
HttpResult,
ResponseFactory,
ErrorCode,
HttpRequestExtras
} from './HttpModel';
const HTTP_TAG = "HttpService";
const DEFAULT_CONNECT_TIMEOUT = 15000;
const DEFAULT_READ_TIMEOUT = 15000;
interface BusinessInfo {
code: number;
msg: string;
}
export class HttpService {
private static instance: HttpService | null = null;
private requestCount: number = 0;
private successCount: number = 0;
private errorCount: number = 0;
private constructor() {}
static getInstance(): HttpService {
if (HttpService.instance === null) {
HttpService.instance = new HttpService();
}
return HttpService.instance;
}
async get(
url: string,
params?: Record<string, string>,
extras?: HttpRequestExtras
): Promise<HttpResult> {
const finalUrl = this.buildUrl(url, params);
const reqOptions: HttpRequestOptions = {
url: finalUrl,
method: http.RequestMethod.GET
};
this.applyExtras(reqOptions, extras);
return this.request(reqOptions);
}
async post(
url: string,
data?: Object | string,
extras?: HttpRequestExtras
): Promise<HttpResult> {
const reqOptions: HttpRequestOptions = {
url: url,
method: http.RequestMethod.POST,
extraData: data
};
this.applyExtras(reqOptions, extras);
return this.request(reqOptions);
}
async put(
url: string,
data?: Object | string,
extras?: HttpRequestExtras
): Promise<HttpResult> {
const reqOptions: HttpRequestOptions = {
url: url,
method: http.RequestMethod.PUT,
extraData: data
};
this.applyExtras(reqOptions, extras);
return this.request(reqOptions);
}
async delete(url: string, extras?: HttpRequestExtras): Promise<HttpResult> {
const reqOptions: HttpRequestOptions = {
url: url,
method: http.RequestMethod.DELETE
};
this.applyExtras(reqOptions, extras);
return this.request(reqOptions);
}
private applyExtras(reqOptions: HttpRequestOptions, extras?: HttpRequestExtras): void {
if (!extras) return;
if (extras.header !== undefined) reqOptions.header = extras.header;
if (extras.extraData !== undefined) reqOptions.extraData = extras.extraData;
if (extras.connectTimeout !== undefined) reqOptions.connectTimeout = extras.connectTimeout;
if (extras.readTimeout !== undefined) reqOptions.readTimeout = extras.readTimeout;
if (extras.expectDataType !== undefined) reqOptions.expectDataType = extras.expectDataType;
}
async request(options: HttpRequestOptions): Promise<HttpResult> {
const url = options.url;
if (!url || url.trim() === "") {
logger.e(HTTP_TAG, "Request failed: url is empty");
return ResponseFactory.httpError(ErrorCode.INVALID_PARAM, "URL cannot be empty", -1, 0);
}
const startTime = Date.now();
this.requestCount++;
const requestId = this.requestCount;
const method = options.method ?? http.RequestMethod.GET;
logger.i(HTTP_TAG, "[%{public}d] --> %{public}s %{public}s", requestId, method, url);
// 构建请求头
const header: Record<string, string> = {};
header["Content-Type"] = "application/json";
header["Accept"] = "application/json";
if (options.header) {
const keys = Object.keys(options.header);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
header[k] = options.header[k];
}
}
const httpRequest = http.createHttp();
const connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
const readTimeout = options.readTimeout ?? DEFAULT_READ_TIMEOUT;
const expectDataType = options.expectDataType ?? http.HttpDataType.STRING;
const requestOptions: http.HttpRequestOptions = {
method: method,
header: header,
connectTimeout: connectTimeout,
readTimeout: readTimeout,
expectDataType: expectDataType
};
if (options.extraData !== undefined) {
requestOptions.extraData = options.extraData;
}
const timeoutMs = connectTimeout + readTimeout + 5000;
return new Promise<HttpResult>((resolve) => {
let resolved = false;
let timeoutId: number = 0;
const safeResolve = (response: HttpResult): void => {
if (!resolved) {
resolved = true;
if (timeoutId !== 0) clearTimeout(timeoutId);
httpRequest.destroy();
resolve(response);
}
};
timeoutId = setTimeout(() => {
if (!resolved) {
this.errorCount++;
logger.e(HTTP_TAG, "[%{public}d] <-- TIMEOUT", requestId);
safeResolve(ResponseFactory.timeout());
}
}, timeoutMs) as number;
httpRequest.request(url, requestOptions, (err: BusinessError, result: http.HttpResponse) => {
clearTimeout(timeoutId);
if (resolved) return;
const costTime = Date.now() - startTime;
if (err) {
this.errorCount++;
const errorCode = err.code;
const errMsg = err.message;
if (errorCode === 200) {
logger.w(HTTP_TAG, "[%{public}d] <-- CANCELLED %{public}dms", requestId, costTime);
safeResolve(ResponseFactory.cancelled());
} else {
logger.e(HTTP_TAG, "[%{public}d] <-- ERROR %{public}d: %{public}s", requestId, errorCode, errMsg);
safeResolve(ResponseFactory.httpError(ErrorCode.NETWORK_ERROR, errMsg, errorCode, costTime));
}
return;
}
this.successCount++;
const httpCode = result.responseCode;
// 处理响应头
const headers: Record<string, string> = {};
const rawHeaderObj = result.header;
if (rawHeaderObj !== null && rawHeaderObj !== undefined) {
const headerStr = JSON.stringify(rawHeaderObj);
if (headerStr !== "{}") {
const pairs = headerStr.substring(1, headerStr.length - 1).split(",");
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
const colonIdx = pair.indexOf(":");
if (colonIdx > 0) {
const hk = pair.substring(1, colonIdx - 1);
const hv = pair.substring(colonIdx + 2, pair.length - 1);
headers[hk] = hv;
}
}
}
}
// 处理响应数据体
let responseData: Object | null = null;
const rawResult = result.result;
const resultType = typeof rawResult;
if (rawResult !== null && rawResult !== undefined) {
if (expectDataType === http.HttpDataType.OBJECT) {
responseData = rawResult as Object;
} else if (expectDataType === http.HttpDataType.STRING) {
if (resultType === "string") {
const strVal = rawResult as string;
const firstChar = strVal.charAt(0);
if (firstChar === "{" || firstChar === "[") {
try {
responseData = JSON.parse(strVal);
} catch {
responseData = strVal as Object;
}
} else {
responseData = strVal as Object;
}
} else {
responseData = rawResult as Object;
}
} else {
responseData = rawResult as Object;
}
}
// 解析业务码和消息
let businessCode = 0;
let businessMsg = "success";
if (responseData !== null) {
const info = this.parseBusinessInfo(responseData);
businessCode = info.code;
businessMsg = info.msg;
}
if (httpCode >= 200 && httpCode < 300) {
if (businessCode === 0) {
logger.i(HTTP_TAG, "[%{public}d] <-- %{public}d success %{public}dms", requestId, httpCode, costTime);
safeResolve(ResponseFactory.success(responseData, httpCode, headers, costTime, businessMsg));
} else {
logger.w(HTTP_TAG, "[%{public}d] <-- %{public}d [BUSINESS ERROR] code=%{public}d", requestId, httpCode, businessCode);
safeResolve(ResponseFactory.businessError(businessCode, businessMsg, costTime));
}
} else {
this.errorCount++;
this.successCount--;
logger.e(HTTP_TAG, "[%{public}d] <-- %{public}d %{public}s", requestId, httpCode, businessMsg);
safeResolve(ResponseFactory.httpError(ErrorCode.SERVER_ERROR, businessMsg, httpCode, costTime));
}
});
});
}
private parseBusinessInfo(data: Object): BusinessInfo {
let code = 0;
let msg = "success";
const dataType = typeof data;
if (dataType === "string") {
const str = data as string;
const codeIdx = str.indexOf('"code"');
if (codeIdx >= 0) {
const segment = str.substring(codeIdx + 6);
const numMatch = segment.match(/^\s*:\s*(-?\d+)/);
if (numMatch !== null) {
const parsed = Number.parseInt(numMatch[1], 10);
if (!Number.isNaN(parsed)) code = parsed;
}
}
const msgIdx = str.indexOf('"msg"');
if (msgIdx >= 0) {
const segment = str.substring(msgIdx + 5);
const strMatch = segment.match(/^\s*:\s*"(.*?)"/);
if (strMatch !== null) msg = strMatch[1];
}
} else if (dataType === "object" && data !== null) {
try {
const jsonStr = JSON.stringify(data);
const codeIdx = jsonStr.indexOf('"code"');
if (codeIdx >= 0) {
const segment = jsonStr.substring(codeIdx + 6);
const numMatch = segment.match(/^\s*:\s*(-?\d+)/);
if (numMatch !== null) {
const parsed = Number.parseInt(numMatch[1], 10);
if (!Number.isNaN(parsed)) code = parsed;
}
}
const msgIdx = jsonStr.indexOf('"msg"');
if (msgIdx >= 0) {
const segment = jsonStr.substring(msgIdx + 5);
const strMatch = segment.match(/^\s*:\s*"(.*?)"/);
if (strMatch !== null) msg = strMatch[1];
}
} catch {}
}
const info: BusinessInfo = { code: code, msg: msg };
return info;
}
private buildUrl(baseUrl: string, params?: Record<string, string>): string {
if (!params) return baseUrl;
const keys = Object.keys(params);
if (keys.length === 0) return baseUrl;
const paramPairs: string[] = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = params[key];
if (value !== null && value !== undefined) {
paramPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
}
if (paramPairs.length === 0) return baseUrl;
const hasQuery = baseUrl.indexOf("?") >= 0;
const separator = hasQuery ? "&" : "?";
return baseUrl + separator + paramPairs.join("&");
}
getRequestStats(): string {
const total = this.requestCount;
const success = this.successCount;
const failed = this.errorCount;
const rate = total > 0 ? ((success / total) * 100).toFixed(1) : "0.0";
return `Total: ${total} | Success: ${success} | Failed: ${failed} | Rate: ${rate}%`;
}
resetStats(): void {
this.requestCount = 0;
this.successCount = 0;
this.errorCount = 0;
}
}
export const httpService = HttpService.getInstance();
四、Flutter 端集成 dio
在 Flutter for OpenHarmony 跨平台工程中,Dart 端通过 MethodChannel 与原生 ETS 模块通信。dio 的适配思路是:在 OpenHarmony 平台上,将 dio 的默认 HTTP 适配器替换为我们的自定义适配器,该适配器通过 MethodChannel 调用上述 ETS 原生网络服务。对于非 OpenHarmony 平台,dio 仍使用其原生适配器,无需任何修改。
4.1 Dart 端网络服务封装
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
/// 统一响应结果
class ApiResponse<T> {
final int code;
final String msg;
final T? data;
final int httpCode;
final int costTime;
ApiResponse({
required this.code,
required this.msg,
this.data,
required this.httpCode,
required this.costTime,
});
bool get isSuccess => code == 0;
}
/// 网络服务(跨平台统一接口)
class NetworkService {
static NetworkService? _instance;
late final Dio _dio;
// MethodChannel 名称,需与原生侧一致
static const String _channelName = 'com.example.app/http_service';
static const MethodChannel _channel = MethodChannel(_channelName);
NetworkService._() {
_dio = Dio();
_dio.options.connectTimeout = const Duration(milliseconds: 15000);
_dio.options.receiveTimeout = const Duration(milliseconds: 15000);
_dio.options.contentType = Headers.jsonContentType;
_dio.options.responseType = ResponseType.json;
// OpenHarmony 平台:注册自定义适配器
if (kIsOpenHarmony) {
_dio.httpClientAdapter = OpenHarmonyHttpAdapter(_channel);
}
}
static NetworkService get instance {
_instance ??= NetworkService._();
return _instance!;
}
Future<ApiResponse<R>> get<R>(
String url, {
Map<String, String>? params,
Map<String, String>? headers,
}) async {
try {
final response = await _dio.get(
url,
queryParameters: params,
options: headers != null ? Options(headers: headers) : null,
);
return _parseResponse<R>(response);
} on DioException catch (e) {
return _handleError(e);
}
}
Future<ApiResponse<R>> post<R>(
String url, {
dynamic data,
Map<String, String>? headers,
}) async {
try {
final response = await _dio.post(
url,
data: data,
options: headers != null ? Options(headers: headers) : null,
);
return _parseResponse<R>(response);
} on DioException catch (e) {
return _handleError(e);
}
}
ApiResponse<R> _parseResponse<R>(Response response) {
final data = response.data;
final code = data['code'] ?? 0;
final msg = data['msg'] ?? 'success';
return ApiResponse(
code: code as int,
msg: msg as String,
data: data['data'] as R?,
httpCode: response.statusCode ?? 0,
costTime: 0,
);
}
ApiResponse<R> _handleError<R>(DioException e) {
int code;
String msg;
switch (e.type) {
case DioExceptionType.connectionTimeout:
code = -1;
msg = 'Connection timeout';
break;
case DioExceptionType.receiveTimeout:
code = -2;
msg = 'Request timeout';
break;
default:
code = -99;
msg = e.message ?? 'Unknown error';
}
return ApiResponse(code: code, msg: msg, httpCode: -1, costTime: -1);
}
}
/// OpenHarmony 平台自定义 dio 适配器
class OpenHarmonyHttpAdapter implements HttpClientAdapter {
final MethodChannel _channel;
OpenHarmonyHttpAdapter(this._channel);
Future<ResponseBody> fetch(
RequestOptions options,
Stream<List<int>>? requestStream,
Future<void>? cancelFuture,
) async {
try {
final result = await _channel.invokeMethod('httpRequest', {
'url': options.uri.toString(),
'method': options.method,
'header': options.headers,
'extraData': options.data,
});
return ResponseBody.fromString(
result['data']?.toString() ?? '{}',
result['httpCode'] ?? 200,
headers: (result['headers'] as Map?)?.map(
(k, v) => MapEntry(k.toString(), [v.toString()]),
),
);
} catch (e) {
throw DioException(
requestOptions: options,
message: e.toString(),
type: DioExceptionType.unknown,
);
}
}
void close({bool force = false}) {}
}
/// 平台判断扩展
extension on bool {
static const kIsOpenHarmony = defaultTargetPlatform == TargetPlatform.openHarmony;
}
4.2 原生侧 MethodChannel 服务注册
在 EntryAbility 中注册 MethodChannel:
import EntryAbility from './entryability/EntryAbility';
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import { httpService } from '../network/HttpService';
const TAG = "EntryAbility";
const DOMAIN = 0xFF00;
export default class MyAbility extends UIAbility {
private httpChannel: wantAgent.MethodChannel | null = null;
onWindowStageCreate(windowStage: window.WindowStage) {
hilog.info(DOMAIN, TAG, "Ability onWindowStageCreate");
// 注册 MethodChannel
const channel = new wantAgent.MethodChannel(
this.context,
"com.example.app/http_service"
);
channel.setMethodCallListener((method: string, args: Record<string, Object>) => {
if (method === 'httpRequest') {
const url = args['url'] as string;
const method = args['method'] as string;
const header = args['header'] as Record<string, string>;
const extraData = args['extraData'];
let requestTask: Promise<HttpResult>;
if (method === 'GET') {
requestTask = httpService.get(url, undefined, { header });
} else if (method === 'POST') {
requestTask = httpService.post(url, extraData, { header });
} else {
return Promise.reject('Unsupported HTTP method');
}
return requestTask.then((result: HttpResult) => {
return {
code: result.code,
msg: result.msg,
data: result.data,
httpCode: result.httpCode,
headers: result.headers,
costTime: result.costTime,
};
});
}
return Promise.reject('Unknown method');
});
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, "Failed to load content: %{public}s", JSON.stringify(err));
return;
}
hilog.info(DOMAIN, TAG, "Succeeded in loading content.");
});
}
}
五、业务层调用实践
无论在 Flutter UI 的哪个位置发起请求,调用方式保持完全一致。以下为页面组件中的典型使用场景:
class NetworkDemoPage extends StatefulWidget {
State<NetworkDemoPage> createState() => _NetworkDemoPageState();
}
class _NetworkDemoPageState extends State<NetworkDemoPage> {
bool _isLoading = false;
String _responseText = '';
int _requestCount = 0;
Future<void> _testGet() async {
setState(() => _isLoading = true);
_requestCount++;
final result = await NetworkService.instance.get(
'https://jsonplaceholder.typicode.com/posts/1',
);
setState(() {
_isLoading = false;
if (result.isSuccess) {
_responseText = '[GET #$_requestCount] SUCCESS\n'
'HTTP: ${result.httpCode}\n'
'Data: ${jsonEncode(result.data)}';
} else {
_responseText = '[GET #$_requestCount] ERROR\n'
'Code: ${result.code}\n'
'Msg: ${result.msg}';
}
});
}
Future<void> _testPost() async {
setState(() => _isLoading = true);
_requestCount++;
final result = await NetworkService.instance.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'title': 'foo',
'body': 'bar',
'userId': 1,
},
);
setState(() {
_isLoading = false;
if (result.isSuccess) {
_responseText = '[POST #$_requestCount] SUCCESS\n'
'HTTP: ${result.httpCode}\n'
'Data: ${jsonEncode(result.data)}';
} else {
_responseText = '[POST #$_requestCount] ERROR\n'
'Code: ${result.code}\n'
'Msg: ${result.msg}';
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Network Demo')),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _isLoading ? null : _testGet,
child: const Text('GET'),
),
ElevatedButton(
onPressed: _isLoading ? null : _testPost,
child: const Text('POST'),
),
],
),
if (_isLoading) const CircularProgressIndicator(),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Text(
_responseText,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
),
],
),
);
}
}
六、总结
通过以上方案,我们在 Flutter for OpenHarmony 跨平台工程中实现了网络请求的完整封装:
-
ETS 原生层(
HttpService.ets):直接调用@ohos.net.http系统 API,是最可靠、最高效的 OpenHarmony 网络实现路径。单例模式确保全局复用同一个 HTTP 连接池,避免资源浪费。统一的HttpResult响应包装器将所有网络场景(成功、失败、超时、取消、业务错误)映射为明确的数字码,方便业务层统一处理。 -
MethodChannel 桥接层:将原生网络能力暴露给 Dart 层。OpenHarmony 平台的 dio 通过自定义
HttpClientAdapter转发请求到原生侧,非 OpenHarmony 平台继续使用原生适配器——这一设计保证了最大程度的代码复用。 -
Flutter 业务层:通过
NetworkService封装 dio,对外提供简洁的get/post方法。ApiResponse<T>泛型类让调用方在编译阶段就知道返回数据的类型,避免了运行时类型转换的风险。 -
平台差异化处理:利用
kIsOpenHarmony(或自定义平台判断常量)在运行时选择适配器,业务层代码完全不需要写if (Platform.isOpenHarmony)分支逻辑。 -
健壮性设计:原生层实现了请求超时兜底机制,防止请求无限挂起;ETS 层通过
safeResolve保证httpRequest.destroy()在所有退出路径上被正确调用,避免内存泄漏;BusinessInfo解析器对无法解析 JSON 的响应做了容错处理,默认为成功。
以上即为 Flutter 三方库 dio 在开源鸿蒙平台上的适配方案,以及基于 OpenHarmony 原生 SDK 的网络请求统一封装。读者可根据自身项目的包管理方式(是否使用 ohpm)和项目结构,将对应代码模块化抽离后直接集成使用。
七、运行截图验证
以下是上述网络请求封装方案在鸿蒙设备上的实际运行效果:
图 1:NetworkDemoPage 主界面(初始状态)

如图所示,页面顶部展示了请求统计数据(Total / Success / Failed / Rate),URL 输入区域允许用户自定义 GET 和 POST 请求的目标地址,POST 请求支持输入 JSON 格式的请求体。
点击 GET 按钮后,系统向 https://jsonplaceholder.typicode.com/posts/1 发起请求,响应数据以格式化 JSON 的形式展示在下方的 Response 区域,同时 Request Log 区域输出了完整的请求链路日志,包含请求 ID、HTTP 方法、目标 URL、HTTP 状态码和耗时等信息。
POST 请求同样成功返回,响应数据中的 code=0 表明业务层判断为成功(非 2xx HTTP 状态或非零业务码的情况会被拦截并展示错误信息)。
GET (params) 按钮演示了自动 URL 参数拼接功能,底层 buildUrl 方法会对参数进行 URL 编码后追加到查询字符串中。
感谢各位阅读!
更多推荐



所有评论(0)