【Harmonyos】Flutter开源鸿蒙跨平台训练营DAY3:Flutter鸿蒙网络请求完整实现指南
分层设计原则层级职责特点表现层UI 渲染、用户交互调用业务层,不直接访问网络业务层业务逻辑、数据组装返回强类型数据,处理业务异常数据层网络通信、数据转换封装底层网络库,统一错误处理外部层后端 API 服务数据源,由后端团队维护网络请求方案对比Flutter 彻量级方案方案优点缺点适用场景HttpClientFlutter 内置,无需依赖功能简单,不支持拦截器简单 GET 请求Dio功能强大,社区活
Flutter 鸿蒙实战:网络请求完整实现指南与最佳实践
本文为开源鸿蒙跨平台训练营 DAY 3 学习笔记,详细讲解如何在 Flutter 鸿蒙跨平台项目中实现完整的网络请求功能,从底层封装到业务集成,涵盖 Dio 详解、拦截器机制、错误处理、数据转换等核心知识点。
目录
- 技术背景与架构设计
- 网络请求方案对比
- 项目结构设计
- 开发环境配置
- 常量管理系统
- 网络请求核心封装
- 数据模型设计
- API 层封装
- 业务层集成
- 网络图片加载优化
- 数据流程分析
- 性能优化与最佳实践
- 常见问题与解决方案
- 完整代码示例
- 总结与展望
技术背景与架构设计
开发环境
| 组件 | 版本 | 说明 |
|---|---|---|
| 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
选择理由:
- 功能完整 - 支持拦截器、请求取消、文件上传下载
- 社区活跃 - GitHub 10k+ stars,维护良好
- 鸿蒙兼容 - 在 OpenHarmony 平台运行稳定
- 类型安全 - 与 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 封装 | 强类型返回、业务层隔离 |
| 性能优化 | 请求合并、缓存、取消、重试 |
关键技术点
- 单例模式 - DioClient 全局共享,避免重复创建
- 拦截器机制 - 统一处理日志、错误、认证
- 工厂函数 - 安全的 JSON 反序列化
- 类型安全 - 强类型 API 返回,减少运行时错误
- 错误处理 - 分层处理网络错误和业务错误
后续扩展方向
- 离线缓存 - 使用 sqlite 或 shared_preferences
- GraphQL - 替代 REST API,按需获取数据
- WebSocket - 实时数据推送
- 状态管理集成 - 与 Provider/Riverpod 结合
- Mock 数据 - 开发阶段使用 Mock 服务
参考资源
更多推荐




所有评论(0)