Flutter 鸿蒙实战:网络请求完整实现指南与最佳实践

本文为开源鸿蒙跨平台训练营 DAY 3 学习笔记,详细讲解如何在 Flutter 鸿蒙跨平台项目中实现完整的网络请求功能,从底层封装到业务集成,涵盖 Dio 详解、拦截器机制、错误处理、数据转换等核心知识点。


目录


技术背景与架构设计

开发环境

组件 版本 说明
Flutter 3.32.4-ohos-0.0.1 鸿蒙定制版 SDK
OpenHarmony 6.0.1 (API 21) 目标平台系统
Dio 5.9.1 HTTP 客户端库
Dart 3.4+ 支持 null-safety

网络请求架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                        网络请求分层架构                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    Presentation Layer (表现层)                   │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │   │
│  │  │  HomePage    │  │  DetailPage  │  │  OtherPages  │          │   │
│  │  └──────────────┘  └──────────────┘  └──────────────┘          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                     │
│                                    ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                     Business Layer (业务层)                     │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │   │
│  │  │ BannerAPI    │  │ ProductAPI   │  │  UserAPI     │          │   │
│  │  └──────────────┘  └──────────────┘  └──────────────┘          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                     │
│                                    ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    Data Layer (数据层)                          │   │
│  │  ┌──────────────────────────────────────────────────────┐       │   │
│  │  │              NetworkService (网络服务)                │       │   │
│  │  │  ┌──────────────────────────────────────────────┐    │       │   │
│  │  │  │  DioInstance (Dio 单例)                       │    │       │   │
│  │  │  │  - Base URL                                  │    │       │   │
│  │  │  │  - Timeout Config                            │    │       │   │
│  │  │  │  - Interceptors                              │    │       │   │
│  │  │  │  - Request Methods (GET/POST/PUT/DELETE)     │    │       │   │
│  │  │  └──────────────────────────────────────────────┘    │       │   │
│  │  └──────────────────────────────────────────────────────┘       │   │
│  │  ┌──────────────────────────────────────────────────────┐       │   │
│  │  │            Models (数据模型)                         │       │   │
│  │  │  BannerItem, ProductItem, UserItem...               │       │   │
│  │  └──────────────────────────────────────────────────────┘       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                     │
│                                    ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    External Layer (外部层)                      │   │
│  │  ┌──────────────────────────────────────────────────────┐       │   │
│  │  │              API Server (后端服务)                   │       │   │
│  │  │  https://meikou-api.itheima.net/                    │       │   │
│  │  └──────────────────────────────────────────────────────┘       │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

分层设计原则

层级 职责 特点
表现层 UI 渲染、用户交互 调用业务层,不直接访问网络
业务层 业务逻辑、数据组装 返回强类型数据,处理业务异常
数据层 网络通信、数据转换 封装底层网络库,统一错误处理
外部层 后端 API 服务 数据源,由后端团队维护

网络请求方案对比

Flutter 彻量级方案

方案 优点 缺点 适用场景
HttpClient Flutter 内置,无需依赖 功能简单,不支持拦截器 简单 GET 请求
Dio 功能强大,社区活跃 依赖第三方包 复杂网络请求,推荐
http 轻量简洁 功能较基础 中小型项目

方案对比表

特性 HttpClient Dio http
拦截器
超时配置
请求取消
文件上传/下载
FormData
日志拦截
轮询请求

最终选择:Dio

选择理由:

  1. 功能完整 - 支持拦截器、请求取消、文件上传下载
  2. 社区活跃 - GitHub 10k+ stars,维护良好
  3. 鸿蒙兼容 - 在 OpenHarmony 平台运行稳定
  4. 类型安全 - 与 Dart null-safety 完美配合

项目结构设计

推荐目录结构

lib/
├── main.dart                      # 应用入口
├── models/                        # 数据模型层
│   └── home/
│       ├── banner_item.dart       # 轮播图模型
│       ├── category_item.dart     # 分类模型
│       └── product_item.dart      # 商品模型
├── network/                       # 网络层
│   ├── core/                      # 核心配置
│   │   ├── network_config.dart    # 网络配置
│   │   └── network_constants.dart # 常量定义
│   ├── services/                  # 服务层
│   │   └── dio_client.dart        # Dio 客户端封装
│   ├── interceptors/              # 拦截器
│   │   ├── log_interceptor.dart   # 日志拦截器
│   │   ├── auth_interceptor.dart  # 认证拦截器
│   │   └── error_interceptor.dart # 错误处理拦截器
│   └── apis/                      # API 接口层
│       └── home_api.dart          # 首页相关 API
├── pages/                         # 页面层
│   └── home/
│       └── home_page.dart         # 首页
└── utils/                         # 工具类
    └── helpers.dart               # 辅助函数

文件命名规范

类型 命名规范 示例
数据模型 lower_snake_case.dart banner_item.dart
服务类 lower_snake_case.dart dio_client.dart
API 类 lower_snake_case.dart home_api.dart
常量类 PascalCase NetworkConstants
配置类 PascalCase NetworkConfig

开发环境配置

安装 Dio 依赖

flutter pub add dio

pubspec.yaml 配置

name: flutter_harmony_app
description: Flutter HarmonyOS App

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # 网络请求
  dio: ^5.9.1

  # 状态管理(可选)
  provider: ^6.1.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true

验证安装

flutter pub get
flutter pub deps

常量管理系统

网络常量定义

lib/network/core/network_constants.dart 中定义网络相关常量:

/// 网络常量定义
///
/// 集中管理所有网络请求相关的常量配置
class NetworkConstants {
  // ==================== 基础配置 ====================

  /// API 基础地址
  ///
  /// 开发环境使用测试地址,生产环境需要切换到正式地址
  static const String baseUrl = 'https://meikou-api.itheima.net/';

  /// 连接超时时间(秒)
  static const int connectTimeout = 15;

  /// 发送超时时间(秒)
  static const int sendTimeout = 15;

  /// 接收超时时间(秒)
  static const int receiveTimeout = 15;

  // ==================== 业务状态码 ====================

  /// 成功状态码
  static const String successCode = '1';

  /// 业务失败状态码
  static const String errorCode = '0';

  /// 登录失效状态码
  static const String tokenExpiredCode = '401';

  // ==================== API 路径 ====================

  /// 首页模块
  static const String bannerList = '/home/banner';
  static const String categoryList = '/home/category';
  static const String productList = '/home/products';

  /// 用户模块
  static const String login = '/user/login';
  static const String userInfo = '/user/info';

  /// 商品模块
  static const String productDetail = '/product/detail';
  static const String productSearch = '/product/search';

  // ==================== HTTP Headers ====================

  /// Content-Type
  static const String contentType = 'application/json';

  /// User-Agent
  static const String userAgent = 'Flutter-Harmony-App/1.0.0';

  // ==================== 分页配置 ====================

  /// 默认页码
  static const int defaultPage = 1;

  /// 默认每页数量
  static const int defaultPageSize = 20;

  // ==================== 缓存配置 ====================

  /// 缓存有效时间(秒)
  static const int cacheTimeout = 300; // 5分钟
}

环境配置管理

lib/network/core/network_config.dart 中管理不同环境的配置:

/// 网络环境枚举
enum NetworkEnvironment {
  /// 开发环境
  dev,

  /// 测试环境
  test,

  /// 生产环境
  prod,
}

/// 网络配置管理
///
/// 根据编译模式自动选择对应的环境配置
class NetworkConfig {
  /// 当前环境
  static NetworkEnvironment _currentEnvironment = NetworkEnvironment.dev;

  /// 获取当前环境
  static NetworkEnvironment get currentEnvironment => _currentEnvironment;

  /// 设置环境(用于测试)
  static void setEnvironment(NetworkEnvironment env) {
    _currentEnvironment = env;
  }

  /// 获取当前环境的基础地址
  static String get baseUrl {
    switch (_currentEnvironment) {
      case NetworkEnvironment.dev:
        return 'https://meikou-api.itheima.net/';
      case NetworkEnvironment.test:
        return 'https://test-api.example.com/';
      case NetworkEnvironment.prod:
        return 'https://api.example.com/';
    }
  }

  /// 是否启用调试日志
  static bool get enableLog {
    switch (_currentEnvironment) {
      case NetworkEnvironment.dev:
        return true;
      case NetworkEnvironment.test:
        return true;
      case NetworkEnvironment.prod:
        return false;
    }
  }

  /// 根据编译模式自动设置环境
  static void initEnvironment() {
    bool isRelease = bool.fromEnvironment('dart.vm.product');
    _currentEnvironment = isRelease
        ? NetworkEnvironment.prod
        : NetworkEnvironment.dev;
  }
}

网络请求核心封装

Dio 基础配置

lib/network/services/dio_client.dart 中创建 Dio 客户端:

import 'package:dio/dio.dart';
import '../core/network_constants.dart';
import '../core/network_config.dart';
import '../interceptors/log_interceptor.dart';
import '../interceptors/error_interceptor.dart';

/// Dio 网络客户端
///
/// 基于 Dio 进行二次封装,提供统一的网络请求接口
class DioClient {
  /// Dio 单例
  static DioClient? _instance;

  /// Dio 实例
  late Dio _dio;

  /// 私有构造函数
  DioClient._internal() {
    _initDio();
  }

  /// 获取单例
  static DioClient get instance {
    _instance ??= DioClient._internal();
    return _instance!;
  }

  /// 初始化 Dio 配置
  void _initDio() {
    _dio = Dio(
      BaseOptions(
        // 基础地址
        baseUrl: NetworkConfig.baseUrl,

        // 连接超时
        connectTimeout: Duration(
          seconds: NetworkConstants.connectTimeout,
        ),

        // 发送超时
        sendTimeout: Duration(
          seconds: NetworkConstants.sendTimeout,
        ),

        // 接收超时
        receiveTimeout: Duration(
          seconds: NetworkConstants.receiveTimeout,
        ),

        // 请求头
        headers: {
          'Content-Type': NetworkConstants.contentType,
          'User-Agent': NetworkConstants.userAgent,
        },

        // 响应类型
        responseType: ResponseType.json,

        // 验证 HTTPS 证书(开发环境可关闭)
        validateStatus: (status) {
          return status != null && status >= 200 && status < 300;
        },
      ),
    );

    // 添加拦截器
    _addInterceptors();
  }

  /// 添加拦截器
  void _addInterceptors() {
    _dio.interceptors.addAll([
      // 日志拦截器
      LogInterceptor(
        request: NetworkConfig.enableLog,
        requestHeader: NetworkConfig.enableLog,
        requestBody: NetworkConfig.enableLog,
        responseHeader: false,
        responseBody: NetworkConfig.enableLog,
        error: NetworkConfig.enableLog,
        logPrint: (object) {
          print('[DIO] $object');
        },
      ),

      // 自定义错误拦截器
      ErrorInterceptor(),
    ]);
  }

  /// 获取 Dio 实例(用于高级用法)
  Dio get dio => _dio;

  // ==================== 请求方法 ====================

  /// GET 请求
  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onReceiveProgress,
  }) async {
    try {
      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress,
      );
      return _handleResponse<T>(response);
    } catch (e) {
      rethrow;
    }
  }

  /// POST 请求
  Future<T> post<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
  }) async {
    try {
      final response = await _dio.post(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return _handleResponse<T>(response);
    } catch (e) {
      rethrow;
    }
  }

  /// PUT 请求
  Future<T> put<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
  }) async {
    try {
      final response = await _dio.put(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress,
      );
      return _handleResponse<T>(response);
    } catch (e) {
      rethrow;
    }
  }

  /// DELETE 请求
  Future<T> delete<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      final response = await _dio.delete(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
        cancelToken: cancelToken,
      );
      return _handleResponse<T>(response);
    } catch (e) {
      rethrow;
    }
  }

  /// 文件上传
  Future<T> upload<T>(
    String path, {
    required FormData formData,
    Options? options,
    CancelToken? cancelToken,
    ProgressCallback? onSendProgress,
  }) async {
    try {
      final response = await _dio.post(
        path,
        data: formData,
        options: options,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
      );
      return _handleResponse<T>(response);
    } catch (e) {
      rethrow;
    }
  }

  /// 文件下载
  Future<void> download(
    String url,
    String savePath, {
    ProgressCallback? onReceiveProgress,
    CancelToken? cancelToken,
  }) async {
    try {
      await _dio.download(
        url,
        savePath,
        onReceiveProgress: onReceiveProgress,
        cancelToken: cancelToken,
      );
    } catch (e) {
      rethrow;
    }
  }

  // ==================== 响应处理 ====================

  /// 处理响应数据
  ///
  /// 统一处理业务状态码,返回业务数据或抛出异常
  T _handleResponse<T>(Response response) {
    try {
      final data = response.data as Map<String, dynamic>;

      // 检查业务状态码
      if (data['code'] == NetworkConstants.successCode) {
        return data['result'] as T;
      }

      // 业务错误
      final message = data['message'] as String? ?? '请求失败';
      throw ApiException(message, data['code']);
    } catch (e) {
      if (e is ApiException) {
        rethrow;
      }
      throw ParseException('数据解析失败: $e');
    }
  }
}

// ==================== 异常定义 ====================

/// API 业务异常
class ApiException implements Exception {
  final String message;
  final String? code;

  ApiException(this.message, [this.code]);

  
  String toString() => 'ApiException: $message (code: $code)';
}

/// 数据解析异常
class ParseException implements Exception {
  final String message;

  ParseException(this.message);

  
  String toString() => 'ParseException: $message';
}

拦截器机制详解

1. 日志拦截器

lib/network/interceptors/log_interceptor.dart 中创建:

import 'package:dio/dio.dart';

/// 日志拦截器
///
/// 打印网络请求和响应的详细信息
class LogInterceptor extends Interceptor {
  final bool request;
  final bool requestHeader;
  final bool requestBody;
  final bool responseHeader;
  final bool responseBody;
  final bool error;
  final void Function(String) logPrint;

  LogInterceptor({
    this.request = true,
    this.requestHeader = true,
    this.requestBody = true,
    this.responseHeader = false,
    this.responseBody = true,
    this.error = true,
    void Function(String)? logPrint,
  }) : logPrint = logPrint ?? print;

  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (!request) return;

    _printLine('╔─────────────── Request ───────────────');
    _printKV('URI', options.uri);
    _printKV('Method', options.method);

    if (requestHeader) {
      _printLine('├── Headers:');
      options.headers.forEach((key, value) {
        _printKV('  $key', value);
      });
    }

    if (requestBody && options.data != null) {
      _printLine('├── Body:');
      _printKV('', options.data);
    }

    if (options.queryParameters.isNotEmpty) {
      _printLine('├── Query Parameters:');
      options.queryParameters.forEach((key, value) {
        _printKV('  $key', value);
      });
    }

    _printLine('╚───────────────────────────────────────');
    _printLine('');

    handler.next(options);
  }

  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (!responseBody) return;

    _printLine('╔─────────────── Response ───────────────');
    _printKV('URI', response.requestOptions.uri);
    _printKV('StatusCode', response.statusCode);

    if (responseHeader) {
      _printLine('├── Headers:');
      response.headers.forEach((key, value) {
        _printKV('  $key', value);
      });
    }

    _printLine('├── Response Data:');
    _printKV('', response.data);

    _printLine('╚───────────────────────────────────────');
    _printLine('');

    handler.next(response);
  }

  
  void onError(DioException err, ErrorInterceptorHandler handler) {
    if (!error) return;

    _printLine('╔════════════════ Error ══════════════════');
    _printKV('URI', err.requestOptions.uri);
    _printKV('Type', err.type);
    _printKV('Message', err.message);

    if (err.response != null) {
      _printKV('StatusCode', err.response?.statusCode);
      _printKV('Data', err.response?.data);
    }

    _printLine('╚═════════════════════════════════════════');
    _printLine('');

    handler.next(err);
  }

  void _printLine(String line) {
    logPrint(line);
  }

  void _printKV(String key, Object? value) {
    logPrint('$key: $value');
  }
}
2. 错误处理拦截器

lib/network/interceptors/error_interceptor.dart 中创建:

import 'package:dio/dio.dart';
import '../core/network_constants.dart';

/// 错误处理拦截器
///
/// 统一处理网络请求错误,转换为用户友好的提示
class ErrorInterceptor extends Interceptor {
  
  void onError(DioException err, ErrorInterceptorHandler handler) {
    final error = _handleError(err);
    handler.reject(DioException(
      requestOptions: err.requestOptions,
      response: err.response,
      error: error,
      type: err.type,
    ));
  }

  /// 处理错误信息
  String _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        return '连接超时,请检查网络设置';

      case DioExceptionType.sendTimeout:
        return '发送超时,请稍后重试';

      case DioExceptionType.receiveTimeout:
        return '接收超时,请稍后重试';

      case DioExceptionType.badResponse:
        return _handleStatusCode(error.response?.statusCode);

      case DioExceptionType.cancel:
        return '请求已取消';

      case DioExceptionType.unknown:
        return _handleUnknownError(error);

      default:
        return '网络异常,请稍后重试';
    }
  }

  /// 处理 HTTP 状态码
  String _handleStatusCode(int? statusCode) {
    switch (statusCode) {
      case 400:
        return '请求参数错误';

      case 401:
        return '登录已过期,请重新登录';

      case 403:
        return '没有权限访问';

      case 404:
        return '请求的资源不存在';

      case 500:
        return '服务器内部错误';

      case 502:
        return '网关错误';

      case 503:
        return '服务暂时不可用';

      case 504:
        return '网关超时';

      default:
        return '请求失败,状态码:$statusCode';
    }
  }

  /// 处理未知错误
  String _handleUnknownError(DioException error) {
    if (error.error is SocketException) {
      return '网络连接失败,请检查网络';
    }
    if (error.error is FormatException) {
      return '数据格式错误';
    }
    return error.message ?? '未知错误';
  }
}

请求方法封装

// 在 DioClient 类中添加以下方法

/// 批量请求
///
/// 同时发起多个请求,全部完成时返回
Future<List<T>> batch<T>(
  List<Future<T>> requests, {
  bool eagerError = false,
}) {
  return Future.wait(requests, eagerError: eagerError);
}

/// 带缓存的 GET 请求
///
/// 先从缓存读取,缓存失效时发起网络请求
Future<T> getCached<T>(
  String path, {
  Map<String, dynamic>? queryParameters,
  Duration cacheDuration = const Duration(minutes: 5),
}) async {
  // TODO: 实现本地缓存逻辑
  return get<T>(path, queryParameters: queryParameters);
}

/// 轮询请求
///
/// 定时发起请求,直到满足条件或超时
Future<T> polling<T>(
  String path, {
  required bool Function(T data) condition,
  Duration interval = const Duration(seconds: 2),
  int maxAttempts = 15,
}) async {
  int attempts = 0;

  while (attempts < maxAttempts) {
    try {
      final result = await get<T>(path);
      if (condition(result)) {
        return result;
      }
    } catch (e) {
      if (attempts == maxAttempts - 1) rethrow;
    }

    attempts++;
    await Future.delayed(interval);
  }

  throw TimeoutException('轮询超时');
}

响应处理与错误处理

// 响应数据封装

/// 统一响应格式
class ApiResponse<T> {
  /// 业务状态码
  final String code;

  /// 提示信息
  final String message;

  /// 业务数据
  final T? data;

  /// 是否成功
  bool get isSuccess => code == NetworkConstants.successCode;

  ApiResponse({
    required this.code,
    required this.message,
    this.data,
  });

  /// 从 JSON 创建
  factory ApiResponse.fromJson(
    Map<String, dynamic> json, {
    T Function(dynamic)? dataParser,
  }) {
    return ApiResponse<T>(
      code: json['code']?.toString() ?? '',
      message: json['message']?.toString() ?? '',
      data: dataParser != null && json['result'] != null
          ? dataParser(json['result'])
          : json['result'] as T?,
    );
  }

  /// 成功响应
  factory ApiResponse.success(T data) {
    return ApiResponse<T>(
      code: NetworkConstants.successCode,
      message: 'success',
      data: data,
    );
  }

  /// 失败响应
  factory ApiResponse.error(String message, [String? code]) {
    return ApiResponse<T>(
      code: code ?? NetworkConstants.errorCode,
      message: message,
    );
  }
}

数据模型设计

JSON 序列化基础

Dart 提供了多种 JSON 序列化方案:

方案 优点 缺点 适用场景
手动序列化 无依赖,灵活 代码量大,易出错 小型项目
json_serializable 类型安全,自动生成 需要代码生成 中大型项目
freezed 功能强大,immutable 学习曲线陡峭 复杂数据模型

工厂构造函数

lib/models/home/banner_item.dart 中定义轮播图模型:

/// 轮播图数据模型
///
/// 表示首页轮播图的单个数据项
class BannerItem {
  /// 唯一标识符
  final String id;

  /// 图片地址(网络 URL)
  final String imageUrl;

  /// 跳转链接(可选)
  final String? targetUrl;

  /// 标题(可选)
  final String? title;

  /// 排序序号(可选)
  final int? sortOrder;

  /// 是否启用(可选)
  final bool? isEnabled;

  BannerItem({
    required this.id,
    required this.imageUrl,
    this.targetUrl,
    this.title,
    this.sortOrder,
    this.isEnabled,
  });

  // ==================== JSON 序列化 ====================

  /// 从 JSON 创建对象
  ///
  /// 使用工厂构造函数从 Map 数据创建 BannerItem 实例
  factory BannerItem.fromJson(Map<String, dynamic> json) {
    return BannerItem(
      id: json['id']?.toString() ?? '',
      imageUrl: json['imgUrl']?.toString() ?? json['imageUrl']?.toString() ?? '',
      targetUrl: json['targetUrl']?.toString(),
      title: json['title']?.toString(),
      sortOrder: json['sortOrder'] as int?,
      isEnabled: json['isEnabled'] as bool?,
    );
  }

  /// 转换为 JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'imgUrl': imageUrl,
      'targetUrl': targetUrl,
      'title': title,
      'sortOrder': sortOrder,
      'isEnabled': isEnabled,
    };
  }

  // ==================== 对象方法 ====================

  /// 复制对象
  BannerItem copyWith({
    String? id,
    String? imageUrl,
    String? targetUrl,
    String? title,
    int? sortOrder,
    bool? isEnabled,
  }) {
    return BannerItem(
      id: id ?? this.id,
      imageUrl: imageUrl ?? this.imageUrl,
      targetUrl: targetUrl ?? this.targetUrl,
      title: title ?? this.title,
      sortOrder: sortOrder ?? this.sortOrder,
      isEnabled: isEnabled ?? this.isEnabled,
    );
  }

  
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is BannerItem && other.id == id;
  }

  
  int get hashCode => id.hashCode;

  
  String toString() {
    return 'BannerItem(id: $id, imageUrl: $imageUrl, title: $title)';
  }
}

空安全处理

/// 安全的 JSON 解析辅助类
class JsonParser {
  /// 安全获取字符串
  static String getString(
    Map<String, dynamic> json,
    String key, {
    String defaultValue = '',
  }) {
    final value = json[key];
    if (value == null) return defaultValue;
    return value.toString();
  }

  /// 安全获取整数
  static int getInt(
    Map<String, dynamic> json,
    String key, {
    int defaultValue = 0,
  }) {
    final value = json[key];
    if (value == null) return defaultValue;
    if (value is int) return value;
    if (value is double) return value.toInt();
    if (value is String) return int.tryParse(value) ?? defaultValue;
    return defaultValue;
  }

  /// 安全获取布尔值
  static bool getBool(
    Map<String, dynamic> json,
    String key, {
    bool defaultValue = false,
  }) {
    final value = json[key];
    if (value == null) return defaultValue;
    if (value is bool) return value;
    if (value is String) {
      final lower = value.toLowerCase();
      return lower == 'true' || lower == '1';
    }
    if (value is int) return value == 1;
    return defaultValue;
  }

  /// 安全获取列表
  static List<T> getList<T>(
    Map<String, dynamic> json,
    String key, {
    List<T>? defaultValue,
  }) {
    final value = json[key];
    if (value == null) return defaultValue ?? [];
    if (value is List) {
      return value.whereType<T>().toList();
    }
    return defaultValue ?? [];
  }
}

API 层封装

lib/network/apis/home_api.dart 中封装首页相关 API:

import '../services/dio_client.dart';
import '../../models/home/banner_item.dart';
import '../core/network_constants.dart';

/// 首页 API
///
/// 封装所有首页相关的网络请求
class HomeApi {
  /// Dio 客户端
  final DioClient _client = DioClient.instance;

  // ==================== 轮播图相关 ====================

  /// 获取轮播图列表
  ///
  /// 返回首页轮播图数据
  ///
  /// 返回:轮播图对象列表
  /// 抛出:[ApiException] 当请求失败时
  Future<List<BannerItem>> getBannerList() async {
    try {
      final result = await _client.get<List<dynamic>>(
        NetworkConstants.bannerList,
      );

      // 将 List<dynamic> 转换为 List<BannerItem>
      return result
          .map((item) => BannerItem.fromJson(item as Map<String, dynamic>))
          .toList();
    } catch (e) {
      throw Exception('获取轮播图失败: $e');
    }
  }

  // ==================== 分类相关 ====================

  /// 获取分类列表
  Future<List<dynamic>> getCategoryList() async {
    try {
      final result = await _client.get<List<dynamic>>(
        NetworkConstants.categoryList,
      );
      return result;
    } catch (e) {
      throw Exception('获取分类列表失败: $e');
    }
  }

  // ==================== 商品相关 ====================

  /// 获取商品列表
  ///
  /// [page] 页码,从 1 开始
  /// [pageSize] 每页数量,默认 20
  Future<List<dynamic>> getProductList({
    int page = 1,
    int pageSize = 20,
  }) async {
    try {
      final result = await _client.get<List<dynamic>>(
        NetworkConstants.productList,
        queryParameters: {
          'page': page,
          'pageSize': pageSize,
        },
      );
      return result;
    } catch (e) {
      throw Exception('获取商品列表失败: $e');
    }
  }

  /// 搜索商品
  ///
  /// [keyword] 搜索关键词
  Future<List<dynamic>> searchProducts(String keyword) async {
    try {
      final result = await _client.get<List<dynamic>>(
        NetworkConstants.productSearch,
        queryParameters: {'keyword': keyword},
      );
      return result;
    } catch (e) {
      throw Exception('搜索商品失败: $e');
    }
  }
}

/// 导出单例
final homeApi = HomeApi();

业务层集成

lib/pages/home/home_page.dart 中集成网络请求:

import 'package:flutter/material.dart';
import '../../network/apis/home_api.dart';
import '../../models/home/banner_item.dart';
import '../../components/home/banner_carousel.dart';

/// 首页
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // ==================== 状态管理 ====================

  /// 轮播图数据
  List<BannerItem> _bannerList = [];

  /// 加载状态
  bool _isLoading = true;

  /// 错误信息
  String? _errorMessage;

  // ==================== 生命周期 ====================

  
  void initState() {
    super.initState();
    _loadData();
  }

  // ==================== 数据加载 ====================

  /// 加载首页数据
  Future<void> _loadData() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      // 并发加载多个数据源
      final results = await Future.wait([
        homeApi.getBannerList(),
        // homeApi.getCategoryList(),
        // homeApi.getProductList(),
      ]);

      setState(() {
        _bannerList = results[0] as List<BannerItem>;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
        _isLoading = false;
      });

      // 显示错误提示
      _showErrorSnackBar(e);
    }
  }

  /// 只刷新轮播图
  Future<void> _refreshBanners() async {
    try {
      final banners = await homeApi.getBannerList();
      setState(() {
        _bannerList = banners;
      });
    } catch (e) {
      _showErrorSnackBar(e);
    }
  }

  // ==================== UI 构建 ====================

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      body: SafeArea(
        child: _buildBody(),
      ),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return _buildLoadingWidget();
    }

    if (_errorMessage != null && _bannerList.isEmpty) {
      return _buildErrorWidget();
    }

    return _buildContentWidget();
  }

  /// 加载中界面
  Widget _buildLoadingWidget() {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(),
          SizedBox(height: 16),
          Text('加载中...'),
        ],
      ),
    );
  }

  /// 错误界面
  Widget _buildErrorWidget() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(
            Icons.error_outline,
            size: 64,
            color: Colors.red,
          ),
          const SizedBox(height: 16),
          Text(
            _errorMessage ?? '加载失败',
            style: const TextStyle(color: Colors.grey),
          ),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: _loadData,
            child: const Text('重新加载'),
          ),
        ],
      ),
    );
  }

  /// 内容界面
  Widget _buildContentWidget() {
    return CustomScrollView(
      slivers: [
        // 轮播图区域
        SliverToBoxAdapter(
          child: BannerCarousel(
            banners: _bannerList,
            height: 280,
            autoPlayDuration: 5,
            showSearchBar: true,
            onSearchTap: _handleSearchTap,
            onBannerTap: _handleBannerTap,
          ),
        ),

        // 下拉刷新按钮
        SliverToBoxAdapter(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: ElevatedButton.icon(
              onPressed: _refreshBanners,
              icon: const Icon(Icons.refresh),
              label: const Text('刷新轮播图'),
            ),
          ),
        ),
      ],
    );
  }

  // ==================== 事件处理 ====================

  /// 搜索框点击
  void _handleSearchTap() {
    debugPrint('跳转到搜索页面');
    // TODO: 跳转搜索页面
  }

  /// 轮播图点击
  void _handleBannerTap(BannerItem banner) {
    debugPrint('点击轮播图: ${banner.title}, 链接: ${banner.targetUrl}');
    // TODO: 根据 banner.targetUrl 跳转
  }

  /// 显示错误提示
  void _showErrorSnackBar(dynamic error) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('加载失败: ${error.toString()}'),
        action: SnackBarAction(
          label: '重试',
          onPressed: _loadData,
        ),
      ),
    );
  }
}

网络图片加载优化

支持网络图片的轮播图组件

修改 lib/components/home/banner_carousel.dart

  /// 构建图片组件
  Widget _buildImage(String imageUrl) {
    // 判断是本地资源还是网络图片
    final isNetworkImage = imageUrl.startsWith('http') ||
        imageUrl.startsWith('https');

    if (isNetworkImage) {
      return _buildNetworkImage(imageUrl);
    } else {
      return _buildAssetImage(imageUrl);
    }
  }

  /// 构建网络图片
  Widget _buildNetworkImage(String imageUrl) {
    return Image.network(
      imageUrl,
      fit: BoxFit.cover,
      width: double.infinity,
      height: double.infinity,

      // 加载中显示
      loadingBuilder: (context, child, loadingProgress) {
        if (loadingProgress == null) return child;

        return Container(
          color: Colors.grey[200],
          child: Center(
            child: CircularProgressIndicator(
              value: loadingProgress.expectedTotalBytes != null
                  ? loadingProgress.cumulativeBytesLoaded /
                      loadingProgress.expectedTotalBytes!
                  : null,
              color: Colors.white,
            ),
          ),
        );
      },

      // 加载失败显示
      errorBuilder: (context, error, stackTrace) {
        return Container(
          color: Colors.grey[300],
          child: const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  Icons.broken_image,
                  size: 48,
                  color: Colors.grey,
                ),
                SizedBox(height: 8),
                Text(
                  '图片加载失败',
                  style: TextStyle(
                    color: Colors.grey,
                    fontSize: 12,
                  ),
                ),
              ],
            ),
          ),
        );
      },

      // 语义化标签
      semanticLabel: '轮播图图片',
    );
  }

  /// 构建本地图片
  Widget _buildAssetImage(String assetPath) {
    return Image.asset(
      assetPath,
      fit: BoxFit.cover,
      width: double.infinity,
      height: double.infinity,
      errorBuilder: (context, error, stackTrace) {
        return Container(
          color: Colors.grey[300],
          child: const Center(
            child: Icon(Icons.broken_image, size: 48, color: Colors.grey),
          ),
        );
      },
    );
  }

数据流程分析

完整数据流程图

┌──────────────────────────────────────────────────────────────────────────┐
│                        网络请求完整流程                                    │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌──────────────┐                                                        │
│  │  HomePage    │                                                        │
│  │  initState() │                                                        │
│  └──────┬───────┘                                                        │
│         │                                                                │
│         │ 调用 API                                                       │
│         ▼                                                                │
│  ┌──────────────┐                                                        │
│  │  HomeApi     │                                                        │
│  │getBannerList()│                                                       │
│  └──────┬───────┘                                                        │
│         │                                                                │
│         │ 调用客户端                                                     │
│         ▼                                                                │
│  ┌──────────────┐     ┌──────────────┐                                  │
│  │ DioClient   │────▶│ LogInterceptor│                                   │
│  │    .get()   │     │  (日志打印)   │                                   │
│  └──────┬───────┘     └──────────────┘                                  │
│         │                                                                │
│         ▼                                                                │
│  ┌──────────────┐     ┌──────────────┐                                  │
│  │    Dio      │────▶│ErrorInterceptor│                                  │
│  │ 发起HTTP请求 │     │  (错误处理)   │                                  │
│  └──────┬───────┘     └──────────────┘                                  │
│         │                                                                │
│         │ 网络传输                                                       │
│         ▼                                                                │
│  ┌──────────────┐                                                        │
│  │  API Server  │                                                        │
│  │ /home/banner │                                                        │
│  └──────┬───────┘                                                        │
│         │                                                                │
│         │ 返回 JSON                                                      │
│         ▼                                                                │
│  ┌──────────────┐                                                        │
│  │_handleResponse│                                                       │
│  │ 检查状态码   │                                                        │
│  └──────┬───────┘                                                        │
│         │                                                                │
│         │ 提取 result                                                    │
│         ▼                                                                │
│  ┌──────────────┐                                                        │
│  │fromJson()    │                                                        │
│  │ 工厂函数转换 │                                                        │
│  └──────┬───────┘                                                        │
│         │                                                                │
│         │ List<BannerItem>                                               │
│         ▼                                                                │
│  ┌──────────────┐                                                        │
│  │  setState    │                                                        │
│  │  更新UI      │                                                        │
│  └──────────────┘                                                        │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

时序图

┌─────────┐    ┌─────────┐    ┌──────────┐    ┌────────┐    ┌─────────┐
│ HomePage│    │ HomeApi │    │DioClient │    │  Dio   │    │  API    │
└────┬────┘    └────┬────┘    └─────┬────┘    └───┬────┘    └────┬────┘
     │              │               │              │              │
     │initState()   │               │              │              │
     ├─────────────>│               │              │              │
     │              │               │              │              │
     │              │getBannerList()│              │              │
     │              ├──────────────>│              │              │
     │              │               │              │              │
     │              │               │  .get()      │              │
     │              │               ├─────────────>│              │
     │              │               │              │              │
     │              │               │              │ HTTP GET     │
     │              │               │              ├─────────────>│
     │              │               │              │              │
     │              │               │              │    JSON      │
     │              │               │              │<─────────────┤
     │              │               │              │              │
     │              │               │<─────────────┤              │
     │              │               │              │              │
     │              │               │fromJSON()    │              │
     │              │               │              │              │
     │              │<──────────────┤              │              │
     │              │               │              │              │
     │<─────────────┤               │              │              │
     │              │               │              │              │
     │setState()    │               │              │              │
     │              │               │              │              │
     │ rebuild      │               │              │              │
     │<─────────────┤               │              │              │

性能优化与最佳实践

1. 请求合并

/// 并发加载多个接口
Future<void> loadHomePageData() async {
  final startTime = DateTime.now();

  // 使用 Future.wait 并发请求
  final results = await Future.wait([
    homeApi.getBannerList(),
    homeApi.getCategoryList(),
    homeApi.getProductList(page: 1),
  ]);

  final endTime = DateTime.now();
  final duration = endTime.difference(startTime);
  debugPrint('并发加载耗时: ${duration.inMilliseconds}ms');

  setState(() {
    _bannerList = results[0];
    _categoryList = results[1];
    _productList = results[2];
  });
}

2. 请求缓存

/// 带内存缓存的请求方法
class CachedRequest {
  static final Map<String, _CacheEntry> _cache = {};

  /// 带缓存的 GET 请求
  static Future<T> get<T>(
    String key,
    Future<T> Function() fetcher, {
    Duration duration = const Duration(minutes: 5),
  }) async {
    // 检查缓存
    final entry = _cache[key];
    if (entry != null && !entry.isExpired) {
      debugPrint('从缓存读取: $key');
      return entry.data as T;
    }

    // 发起请求
    final data = await fetcher();

    // 写入缓存
    _cache[key] = _CacheEntry(data, DateTime.now().add(duration));

    return data;
  }

  /// 清除缓存
  static void clear(String key) {
    _cache.remove(key);
  }

  /// 清除所有缓存
  static void clearAll() {
    _cache.clear();
  }
}

class _CacheEntry {
  final dynamic data;
  final DateTime expireTime;

  _CacheEntry(this.data, this.expireTime);

  bool get isExpired => DateTime.now().isAfter(expireTime);
}

3. 请求取消

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  CancelToken? _cancelToken;

  
  void initState() {
    super.initState();
    _loadData();
  }

  
  void dispose() {
    // 页面销毁时取消请求
    _cancelToken?.cancel();
    super.dispose();
  }

  Future<void> _loadData() async {
    // 取消之前的请求
    _cancelToken?.cancel();
    _cancelToken = CancelToken();

    try {
      final result = await homeApi.getBannerList(
        cancelToken: _cancelToken,
      );
      setState(() {
        _bannerList = result;
      });
    } catch (e) {
      if (e is DioException && e.type == DioExceptionType.cancel) {
        debugPrint('请求已取消');
      } else {
        _showError(e);
      }
    }
  }
}

4. 重试机制

/// 带重试的请求方法
Future<T> retryRequest<T>(
  Future<T> Function() request, {
  int maxAttempts = 3,
  Duration delay = const Duration(seconds: 1),
}) async {
  int attempts = 0;

  while (attempts < maxAttempts) {
    try {
      return await request();
    } catch (e) {
      attempts++;
      if (attempts >= maxAttempts) rethrow;

      debugPrint('请求失败,${delay.inSeconds}秒后重试 ($attempts/$maxAttempts)');
      await Future.delayed(delay);
    }
  }

  throw Exception('请求失败,已达最大重试次数');
}

// 使用示例
final banners = await retryRequest(
  () => homeApi.getBannerList(),
  maxAttempts: 3,
);

5. 请求节流

/// 请求节流器
class RequestThrottle {
  final Duration delay;
  DateTime? _lastRequestTime;

  RequestThrottle({this.delay = const Duration(milliseconds: 500)});

  Future<T> throttle<T>(Future<T> Function() request) async {
    final now = DateTime.now();

    if (_lastRequestTime != null) {
      final elapsed = now.difference(_lastRequestTime!);
      if (elapsed < delay) {
        await Future.delayed(delay - elapsed);
      }
    }

    _lastRequestTime = DateTime.now();
    return await request();
  }
}

常见问题与解决方案

问题 1:网络权限配置

鸿蒙平台需要在 module.json5 中配置网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

问题 2:HTTPS 证书校验

开发环境禁用证书校验(不推荐生产环境):

Dio(
  baseOptions: BaseOptions(
    validateCertificate: false, // 仅开发环境使用
  ),
)

生产环境配置证书固定:

_dio.interceptors.add(
  SSLPinning(
    allowedCertificates: [
      'certificates/my_cert.pem',
    ],
  ),
);

问题 3:JSON 解析异常

安全解析方案:

/// 安全的 JSON 解析
T? safeParse<T>(
  Map<String, dynamic> json,
  String key,
  T Function(dynamic) parser,
) {
  try {
    final value = json[key];
    if (value == null) return null;
    return parser(value);
  } catch (e) {
    debugPrint('JSON 解析失败: $key, error: $e');
    return null;
  }
}

问题 4:超时设置不合理

根据接口类型设置不同超时:

class TimeoutConfig {
  static const Duration fastRequest = Duration(seconds: 5);
  static const Duration normalRequest = Duration(seconds: 15);
  static const Duration slowRequest = Duration(seconds: 30);
  static const Duration uploadRequest = Duration(minutes: 5);
}

// 使用示例
final dio = Dio(
  BaseOptions(
    connectTimeout: TimeoutConfig.normalRequest,
    receiveTimeout: TimeoutConfig.slowRequest,
  ),
);

完整代码示例

main.dart 入口配置

import 'package:flutter/material.dart';
import 'network/core/network_config.dart';
import 'pages/home/home_page.dart';

void main() {
  // 初始化网络配置
  NetworkConfig.initEnvironment();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Harmony App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

总结与展望

本文要点回顾

本文详细介绍了在 Flutter 鸿蒙跨平台项目中实现完整网络请求功能的流程,涵盖以下要点:

模块 核心内容
架构设计 分层架构、职责分离、可维护性
Dio 封装 单例模式、拦截器、统一错误处理
常量管理 环境配置、业务常量、API 路径
数据模型 工厂函数、JSON 序列化、空安全
API 封装 强类型返回、业务层隔离
性能优化 请求合并、缓存、取消、重试

关键技术点

  1. 单例模式 - DioClient 全局共享,避免重复创建
  2. 拦截器机制 - 统一处理日志、错误、认证
  3. 工厂函数 - 安全的 JSON 反序列化
  4. 类型安全 - 强类型 API 返回,减少运行时错误
  5. 错误处理 - 分层处理网络错误和业务错误

后续扩展方向

  1. 离线缓存 - 使用 sqlite 或 shared_preferences
  2. GraphQL - 替代 REST API,按需获取数据
  3. WebSocket - 实时数据推送
  4. 状态管理集成 - 与 Provider/Riverpod 结合
  5. Mock 数据 - 开发阶段使用 Mock 服务

参考资源


Logo

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

更多推荐