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 默认的 IOHttpClientAdapterBrowserHttpClientAdapter
  • 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 跨平台工程中实现了网络请求的完整封装:

  1. ETS 原生层HttpService.ets):直接调用 @ohos.net.http 系统 API,是最可靠、最高效的 OpenHarmony 网络实现路径。单例模式确保全局复用同一个 HTTP 连接池,避免资源浪费。统一的 HttpResult 响应包装器将所有网络场景(成功、失败、超时、取消、业务错误)映射为明确的数字码,方便业务层统一处理。

  2. MethodChannel 桥接层:将原生网络能力暴露给 Dart 层。OpenHarmony 平台的 dio 通过自定义 HttpClientAdapter 转发请求到原生侧,非 OpenHarmony 平台继续使用原生适配器——这一设计保证了最大程度的代码复用。

  3. Flutter 业务层:通过 NetworkService 封装 dio,对外提供简洁的 get/post 方法。ApiResponse<T> 泛型类让调用方在编译阶段就知道返回数据的类型,避免了运行时类型转换的风险。

  4. 平台差异化处理:利用 kIsOpenHarmony(或自定义平台判断常量)在运行时选择适配器,业务层代码完全不需要写 if (Platform.isOpenHarmony) 分支逻辑。

  5. 健壮性设计:原生层实现了请求超时兜底机制,防止请求无限挂起;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 编码后追加到查询字符串中。

感谢各位阅读!


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐