# Flutter网络请求实战:缓存策略、弱网适配与问题排查
Flutter网络请求实战:缓存策略、弱网适配与问题排查
在完成网络请求基础封装、进阶功能(加密、多环境、断点续传)后,应用的网络交互仍需面对两大核心挑战:一是复杂网络环境下的用户体验保障(如弱网、断网时的流畅性),二是线上问题的快速定位与解决。本文将围绕“提升稳定性”与“降低排障成本”两大目标,展开三大核心内容:可落地的网络缓存策略、全场景弱网/断网适配方案、线上请求监控与常见问题排查手册,帮助开发者构建“高可用、易维护”的Flutter网络交互体系。
一、网络缓存策略:减少无效请求,提升弱网体验
网络缓存是优化网络请求的核心手段之一,通过缓存接口返回的非实时数据(如首页列表、商品详情、分类信息),可实现“离线访问”“弱网快速加载”,同时减少后端接口压力。Flutter网络缓存需遵循“按需缓存、精准失效”原则,避免缓存数据过期导致的业务异常。本节将介绍3种实战缓存方案,并给出具体实现。
1. 缓存方案选型:根据业务场景匹配
| 缓存方案 | 核心原理 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 内存缓存 | 将数据缓存到内存Map中,应用存活期间有效 | 读取速度最快、实现最简单 | 应用重启后失效、占用内存资源 | 短期频繁访问的数据(如当前页面列表、临时配置) |
| 本地持久化缓存(Hive/SQLite) | 将数据序列化后存入本地数据库,持久化存储 | 持久化有效、支持复杂查询、可控制缓存大小 | 实现稍复杂、读取速度慢于内存缓存 | 离线访问需求的数据(如用户历史记录、离线文章)、长期复用数据 |
| HTTP缓存(Cache-Control) | 通过HTTP响应头(Cache-Control、Expires)控制缓存,依赖dio拦截器实现 | 符合HTTP标准、无需手动管理缓存失效 | 灵活性低、依赖后端配置、不支持自定义缓存规则 | 后端已配置缓存策略的接口(如静态资源、不变的配置接口) |
实际开发中推荐“内存缓存+本地持久化缓存”组合方案:内存缓存提升访问速度,本地缓存保障离线可用,兼顾性能与体验。
2. 实战实现:二级缓存(内存+Hive)
基于此前的NetworkUtil封装,新增二级缓存功能,核心逻辑:请求前先查内存缓存,无则查本地缓存,均无则发起网络请求;请求成功后同步更新内存缓存与本地缓存。
步骤1:定义缓存模型与工具类
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:flutter/foundation.dart';
// 缓存数据模型(含过期时间)
class CacheModel {
final dynamic data; // 缓存数据
final int timestamp; // 缓存时间戳(毫秒)
final int maxAge; // 缓存有效期(秒)
CacheModel({
required this.data,
required this.timestamp,
required this.maxAge,
});
// 转换为Map,用于Hive存储
Map<String, dynamic> toMap() {
return {
'data': data,
'timestamp': timestamp,
'maxAge': maxAge,
};
}
// 从Map解析
factory CacheModel.fromMap(Map<String, dynamic> map) {
return CacheModel(
data: map['data'],
timestamp: map['timestamp'],
maxAge: map['maxAge'],
);
}
// 检查缓存是否过期
bool get isExpired {
final now = DateTime.now().millisecondsSinceEpoch;
return (now - timestamp) > (maxAge * 1000);
}
}
// 缓存工具类(单例)
class CacheUtil {
static final CacheUtil _instance = CacheUtil._internal();
factory CacheUtil() => _instance;
CacheUtil._internal();
// 内存缓存(Map<缓存key, CacheModel>)
final Map<String, CacheModel> _memoryCache = {};
// Hive本地缓存箱(需先初始化)
late Box<Map<String, dynamic>> _cacheBox;
// 初始化Hive缓存箱
Future<void> init() async {
_cacheBox = await Hive.openBox<Map<String, dynamic>>('network_cache');
}
// 生成缓存key(基于请求方法+URL+参数,确保唯一性)
String generateCacheKey(String method, String url, {Map<String, dynamic>? params}) {
final paramsStr = params != null ? json.encode(params) : '';
return '$method\_$url\_$paramsStr';
}
// 读取缓存(先查内存,再查本地)
CacheModel? getCache(String key) {
// 1. 查内存缓存
if (_memoryCache.containsKey(key)) {
final cache = _memoryCache[key];
if (cache != null && !cache.isExpired) {
return cache;
} else {
_memoryCache.remove(key); // 内存缓存过期,移除
}
}
// 2. 查本地缓存
if (_cacheBox.containsKey(key)) {
final map = _cacheBox.get(key);
if (map != null) {
final cache = CacheModel.fromMap(map);
if (!cache.isExpired) {
// 本地缓存有效,同步到内存
_memoryCache[key] = cache;
return cache;
} else {
_cacheBox.delete(key); // 本地缓存过期,删除
}
}
}
return null;
}
// 保存缓存(同时存内存和本地)
void saveCache(String key, CacheModel cache) {
// 1. 存内存
_memoryCache[key] = cache;
// 2. 存本地
_cacheBox.put(key, cache.toMap());
}
// 删除指定缓存
void deleteCache(String key) {
_memoryCache.remove(key);
_cacheBox.delete(key);
}
// 清空所有缓存
void clearAllCache() {
_memoryCache.clear();
_cacheBox.clear();
}
// 清理过期缓存(可定期调用,如应用启动时)
void cleanExpiredCache() {
// 清理内存过期缓存
_memoryCache.removeWhere((key, cache) => cache.isExpired);
// 清理本地过期缓存
final keys = _cacheBox.keys.toList();
for (final key in keys) {
final map = _cacheBox.get(key);
if (map != null) {
final cache = CacheModel.fromMap(map);
if (cache.isExpired) {
_cacheBox.delete(key);
}
}
}
}
}
final cacheUtil = CacheUtil();
步骤2:在应用入口初始化缓存工具
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'cache_util.dart';
import 'config/env_config.dart';
void main() async {
// 初始化Hive
await Hive.initFlutter();
// 初始化缓存工具
await cacheUtil.init();
// 清理过期缓存
cacheUtil.cleanExpiredCache();
// 初始化环境配置(此前实现的逻辑)
final String envStr = const String.fromEnvironment('ENV', defaultValue: 'dev');
EnvType envType;
switch (envStr) {
case 'test':
envType = EnvType.test;
break;
case 'prod':
envType = EnvType.prod;
break;
default:
envType = EnvType.dev;
}
EnvManager.init(envType);
runApp(const MyApp());
}
// ... MyApp等后续代码不变
步骤3:在NetworkUtil中集成缓存功能
import 'cache_util.dart';
class NetworkUtil {
// ... 此前的单例、_dio初始化等逻辑不变
// 新增带缓存的GET请求方法
Future<T?> getWithCache<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
int maxAge = 300, // 默认缓存5分钟(300秒)
}) async {
// 1. 生成缓存key
final cacheKey = cacheUtil.generateCacheKey('GET', path, params: params);
// 2. 读取缓存
final cache = cacheUtil.getCache(cacheKey);
if (cache != null) {
// 缓存有效,直接返回
return cache.data as T?;
}
// 3. 缓存无效/无缓存,发起网络请求
try {
final response = await _dio.get(
path,
queryParameters: params,
options: options,
cancelToken: _cancelToken,
);
// 4. 保存缓存(请求成功后)
cacheUtil.saveCache(
cacheKey,
CacheModel(
data: response.data,
timestamp: DateTime.now().millisecondsSinceEpoch,
maxAge: maxAge,
),
);
return response.data as T?;
} catch (e) {
rethrow;
}
}
// 新增缓存失效方法(如数据更新后,主动让对应缓存失效)
void invalidateCache(String method, String path, {Map<String, dynamic>? params}) {
final cacheKey = cacheUtil.generateCacheKey(method, path, params: params);
cacheUtil.deleteCache(cacheKey);
}
// ... 其他原有方法不变
}
步骤4:使用带缓存的请求
// 示例:获取商品列表,缓存10分钟
Future<void> fetchGoodsList() async {
try {
final data = await netUtil.getWithCache<List<dynamic>>(
'goods/list',
params: {'categoryId': 1},
maxAge: 600, // 缓存10分钟
);
// ... 更新UI逻辑
} catch (e) {
// ... 异常处理
}
}
// 示例:更新商品后,让商品列表缓存失效
Future<void> updateGoods(Goods goods) async {
try {
await netUtil.post(
'goods/update',
data: goods.toJson(),
);
// 主动失效商品列表缓存,确保下次请求获取最新数据
netUtil.invalidateCache('GET', 'goods/list', params: {'categoryId': 1});
} catch (e) {
// ... 异常处理
}
}
二、弱网/断网适配:保障极端网络环境下的用户体验
移动应用常面临弱网(如地铁、电梯)、断网等极端网络环境,若未做适配,会出现“请求一直加载”“点击无响应”“数据丢失”等问题。弱网/断网适配的核心目标是:让用户感知清晰、操作可恢复、数据不丢失。本节将从“网络状态监听”“请求状态适配”“离线操作队列”三个维度,给出完整实现方案。
1. 网络状态监听:实时感知网络变化
使用connectivity_plus库监听网络状态变化,为后续适配逻辑提供依据。
步骤1:添加依赖
dependencies:
connectivity_plus: ^4.0.1 # 监听网络状态
步骤2:实现网络状态监听工具类
import 'package:connectivity_plus/connectivity_plus.dart';
import 'dart:async';
// 网络状态枚举
enum NetworkState {
connected, // 已连接
disconnected, // 未连接
connecting, // 连接中
}
// 网络状态监听工具类(单例)
class NetworkMonitor {
static final NetworkMonitor _instance = NetworkMonitor._internal();
factory NetworkMonitor() => _instance;
NetworkMonitor._internal();
final Connectivity _connectivity = Connectivity();
late StreamSubscription<ConnectivityResult> _subscription;
final StreamController<NetworkState> _stateController = StreamController<NetworkState>.broadcast();
NetworkState _currentState = NetworkState.connecting;
// 初始化网络监听
Future<void> init() async {
// 初始网络状态检测
final result = await _connectivity.checkConnectivity();
_updateState(result);
// 监听网络状态变化
_subscription = _connectivity.onConnectivityChanged.listen((result) {
_updateState(result);
});
}
// 更新网络状态
void _updateState(ConnectivityResult result) {
NetworkState newState;
if (result == ConnectivityResult.none) {
newState = NetworkState.disconnected;
} else {
newState = NetworkState.connected;
}
if (newState != _currentState) {
_currentState = newState;
_stateController.add(newState); // 发送状态变化通知
}
}
// 获取当前网络状态
NetworkState get currentState => _currentState;
// 获取网络状态流(外部可监听)
Stream<NetworkState> get stateStream => _stateController.stream;
// 销毁监听
void dispose() {
_subscription.cancel();
_stateController.close();
}
}
final networkMonitor = NetworkMonitor();
步骤3:在应用入口初始化网络监听
void main() async {
// ... 此前的Hive、缓存初始化逻辑
// 初始化网络监听
await networkMonitor.init();
// ... 环境配置初始化、runApp逻辑
}
2. 弱网/断网适配:请求状态与UI联动
基于网络状态监听,实现三大适配逻辑:断网时禁止发起非必要请求、弱网时显示加载提示与重试按钮、网络恢复时自动重试失败的请求。
步骤1:在NetworkUtil中添加弱网适配(超时配置、网络状态校验)
class NetworkUtil {
// ... 原有逻辑不变
// 新增带弱网适配的请求方法(通用封装)
Future<T?> requestWithAdapt<T>(
String path, {
required String method,
Map<String, dynamic>? params,
dynamic data,
Options? options,
bool allowOffline = false, // 是否允许离线发起(如提交表单)
}) async {
// 1. 校验网络状态(非离线允许的请求,断网时直接抛出异常)
if (!allowOffline && networkMonitor.currentState == NetworkState.disconnected) {
throw DioException(
requestOptions: RequestOptions(path: path),
type: DioExceptionType.connectionError,
message: '当前无网络,请检查网络连接',
);
}
// 2. 弱网超时配置(弱网环境下延长超时时间)
final weakNetOptions = Options(
method: method,
sendTimeout: Duration(milliseconds: networkMonitor.currentState == NetworkState.connected ? 5000 : 15000),
receiveTimeout: Duration(milliseconds: networkMonitor.currentState == NetworkState.connected ? 5000 : 15000),
);
final finalOptions = weakNetOptions.copyWith(
headers: options?.headers,
contentType: options?.contentType,
responseType: options?.responseType,
);
// 3. 发起请求
try {
late Response response;
switch (method.toUpperCase()) {
case 'GET':
response = await _dio.get(path, queryParameters: params, options: finalOptions, cancelToken: _cancelToken);
break;
case 'POST':
response = await _dio.post(path, data: data, queryParameters: params, options: finalOptions, cancelToken: _cancelToken);
break;
// ... 其他方法(PUT/DELETE)类似
default:
throw DioException(
requestOptions: RequestOptions(path: path),
type: DioExceptionType.unknown,
message: '不支持的请求方法',
);
}
return response.data as T?;
} catch (e) {
rethrow;
}
}
// 封装带适配的GET/POST方法
Future<T?> getWithAdapt<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
}) =>
requestWithAdapt<T>(
path,
method: 'GET',
params: params,
options: options,
allowOffline: false,
);
Future<T?> postWithAdapt<T>(
String path, {
dynamic data,
Map<String, dynamic>? params,
Options? options,
bool allowOffline = true, // 提交类请求允许离线发起(后续加入队列)
}) =>
requestWithAdapt<T>(
path,
method: 'POST',
data: data,
params: params,
options: options,
allowOffline: allowOffline,
);
}
步骤2:实现离线操作队列(保障数据不丢失)
对于允许离线发起的请求(如提交表单、上传数据),断网时将请求信息存入本地队列,网络恢复时自动重试。
import 'dart:convert';
import 'package:hive/hive.dart';
import 'network_util.dart';
import 'network_monitor.dart';
// 离线请求模型
class OfflineRequest {
final String id; // 唯一标识
final String path;
final String method;
final dynamic data;
final Map<String, dynamic>? params;
final Map<String, dynamic>? headers;
final DateTime createTime; // 创建时间
OfflineRequest({
required this.id,
required this.path,
required this.method,
required this.data,
this.params,
this.headers,
required this.createTime,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'path': path,
'method': method,
'data': data,
'params': params,
'headers': headers,
'createTime': createTime.millisecondsSinceEpoch,
};
}
factory OfflineRequest.fromMap(Map<String, dynamic> map) {
return OfflineRequest(
id: map['id'],
path: map['path'],
method: map['method'],
data: map['data'],
params: map['params'],
headers: map['headers'],
createTime: DateTime.fromMillisecondsSinceEpoch(map['createTime']),
);
}
}
// 离线操作队列工具类
class OfflineQueue {
static final OfflineQueue _instance = OfflineQueue._internal();
factory OfflineQueue() => _instance;
OfflineQueue._internal();
late Box<Map<String, dynamic>> _queueBox;
bool _isProcessing = false; // 是否正在处理队列
// 初始化
Future<void> init() async {
_queueBox = await Hive.openBox<Map<String, dynamic>>('offline_queue');
// 监听网络恢复,自动处理队列
networkMonitor.stateStream.listen((state) {
if (state == NetworkState.connected) {
processQueue();
}
});
}
// 添加请求到队列
Future<void> addRequest(OfflineRequest request) async {
await _queueBox.put(request.id, request.toMap());
}
// 处理队列(网络恢复时调用)
Future<void> processQueue() async {
if (_isProcessing || _queueBox.isEmpty) return;
_isProcessing = true;
try {
// 获取所有离线请求
final requestMaps = _queueBox.values.toList();
for (final map in requestMaps) {
final request = OfflineRequest.fromMap(map);
try {
// 发起重试请求
switch (request.method.toUpperCase()) {
case 'POST':
await netUtil.post(
request.path,
data: request.data,
params: request.params,
options: Options(headers: request.headers),
);
break;
case 'PUT':
await netUtil.put(
request.path,
data: request.data,
params: request.params,
options: Options(headers: request.headers),
);
break;
// ... 其他支持的方法
}
// 重试成功,从队列中移除
await _queueBox.delete(request.id);
} catch (e) {
print('离线请求重试失败:${request.id},错误:$e');
// 重试失败,保留在队列中,等待下次网络恢复
continue;
}
}
} catch (e) {
print('处理离线队列失败:$e');
} finally {
_isProcessing = false;
}
}
// 获取队列长度(用于UI显示)
int get queueLength => _queueBox.length;
// 清空队列
Future<void> clearQueue() async {
await _queueBox.clear();
}
}
final offlineQueue = OfflineQueue();
步骤3:在应用入口初始化离线队列
void main() async {
// ... 此前的Hive、缓存、网络监听初始化逻辑
// 初始化离线队列
await offlineQueue.init();
// ... 环境配置初始化、runApp逻辑
}
步骤4:UI层适配(显示网络状态、重试按钮、离线队列提示)
import 'package:flutter/material.dart';
import 'network_monitor.dart';
import 'offline_queue.dart';
import 'network_util.dart';
class NetworkAdaptDemo extends StatefulWidget {
@override
_NetworkAdaptDemoState createState() => _NetworkAdaptDemoState();
}
class _NetworkAdaptDemoState extends State<NetworkAdaptDemo> {
List<dynamic> _data = [];
bool _isLoading = false;
String? _errorMsg;
@override
void initState() {
super.initState();
// 监听网络状态变化,更新UI
networkMonitor.stateStream.listen((state) {
setState(() {
if (state == NetworkState.disconnected) {
_errorMsg = '当前无网络,请检查连接';
} else if (state == NetworkState.connected) {
_errorMsg = null;
// 网络恢复,重新加载数据
fetchData();
}
});
});
fetchData();
}
Future<void> fetchData() async {
setState(() {
_isLoading = true;
_errorMsg = null;
});
try {
// 使用带弱网适配的GET请求
final data = await netUtil.getWithAdapt<List<dynamic>>('data/list');
setState(() {
_data = data ?? [];
});
} catch (e) {
setState(() {
_errorMsg = (e as DioException).message;
});
} finally {
setState(() {
_isLoading = false;
});
}
}
// 提交表单(允许离线,加入离线队列)
Future<void> submitForm() async {
final requestId = DateTime.now().millisecondsSinceEpoch.toString();
try {
await netUtil.postWithAdapt(
'form/submit',
data: {'content': '测试离线提交'},
allowOffline: true,
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('提交成功')));
} catch (e) {
if ((e as DioException).type == DioExceptionType.connectionError) {
// 断网,加入离线队列
final request = OfflineRequest(
id: requestId,
path: 'form/submit',
method: 'POST',
data: {'content': '测试离线提交'},
createTime: DateTime.now(),
);
await offlineQueue.addRequest(request);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加入离线队列,网络恢复后自动提交')));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('提交失败:${e.message}')));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('弱网/断网适配示例'),
actions: [
// 显示离线队列长度
StreamBuilder<int>(
stream: Stream.periodic(Duration(seconds: 1)).map((_) => offlineQueue.queueLength),
initialData: offlineQueue.queueLength,
builder: (context, snapshot) {
final length = snapshot.data ?? 0;
if (length == 0) return SizedBox.shrink();
return Padding(
padding: EdgeInsets.only(right: 16),
child: Center(
child: Badge(
label: Text(length.toString()),
child: Icon(Icons.queue),
),
),
);
},
),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
ElevatedButton(
onPressed: submitForm,
child: Text('提交表单(支持离线)'),
),
SizedBox(height: 20),
if (_isLoading)
CircularProgressIndicator()
else if (_errorMsg != null)
Column(
children: [
Text(_errorMsg!, style: TextStyle(color: Colors.red)),
SizedBox(height: 10),
ElevatedButton(
onPressed: fetchData,
child: Text('重试'),
),
],
)
else
Expanded(
child: ListView.builder(
itemCount: _data.length,
itemBuilder: (context, index) => ListTile(title: Text(_data[index]['title'])),
),
),
],
),
),
);
}
}
三、网络请求监控与常见问题排查
线上网络请求难免出现问题(如接口超时、数据异常、加密失败),需通过“请求监控”快速定位问题,同时掌握常见问题的解决方案。本节将介绍请求日志监控实现,以及10类高频问题的排查思路。
1. 网络请求监控:日志采集与上报
通过dio拦截器采集请求日志(请求参数、响应数据、异常信息、网络状态、耗时),并上报到后端监控平台(如阿里云ARMS、友盟+),便于线上问题定位。
步骤1:实现日志采集拦截器
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'network_monitor.dart';
// 日志模型
class RequestLog {
final String requestId; // 唯一请求ID
final String method;
final String url;
final Map<String, dynamic>? params;
final dynamic data;
final Map<String, dynamic>? headers;
final int? statusCode;
final dynamic responseData;
final String? errorMsg;
final String? errorStack;
final int duration; // 请求耗时(毫秒)
final String networkType; // 网络类型(wifi/4g/5g/none)
final DateTime requestTime;
RequestLog({
required this.requestId,
required this.method,
required this.url,
this.params,
this.data,
this.headers,
this.statusCode,
this.responseData,
this.errorMsg,
this.errorStack,
required this.duration,
required this.networkType,
required this.requestTime,
});
Map<String, dynamic> toJson() {
return {
'requestId': requestId,
'method': method,
'url': url,
'params': params,
'data': data,
'headers': headers,
'statusCode': statusCode,
'responseData': responseData,
'errorMsg': errorMsg,
'errorStack': errorStack,
'duration': duration,
'networkType': networkType,
'requestTime': requestTime.toIso8601String(),
};
}
}
// 日志监控拦截器
class LogMonitorInterceptor extends Interceptor {
// 上报日志(实际项目中替换为后端监控接口)
void _reportLog(RequestLog log) {
if (kReleaseMode) {
// release模式下上报到监控平台
print('上报请求日志:${log.toJson()}');
// 示例:通过dio上报
// netUtil.post('/monitor/log', data: log.toJson(), allowOffline: true);
} else {
// debug模式下打印日志
print('=== 请求日志 ===');
print('请求ID:${log.requestId}');
print('请求时间:${log.requestTime}');
print('网络类型:${log.networkType}');
print('请求信息:${log.method} ${"${log.url}?${log.params ?? {}}"}');
print('请求体:${log.data}');
if (log.errorMsg != null) {
print('请求失败:${log.errorMsg}');
print('错误堆栈:${log.errorStack}');
} else {
print('响应状态码:${log.statusCode}');
print('响应数据:${log.responseData}');
}
print('请求耗时:${log.duration}ms');
print('================');
}
}
// 获取网络类型描述
String _getNetworkType() {
switch (networkMonitor.currentState) {
case NetworkState.disconnected:
return 'none';
case NetworkState.connected:
// 实际可通过connectivity_plus获取具体网络类型(wifi/4g/5g)
return 'unknown_connected';
case NetworkState.connecting:
return 'connecting';
}
}
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 生成唯一请求ID
options.extra['requestId'] = DateTime.now().millisecondsSinceEpoch.toString();
// 记录请求开始时间
options.extra['startTime'] = DateTime.now().millisecondsSinceEpoch;
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final requestId = response.requestOptions.extra['requestId'] as String;
final startTime = response.requestOptions.extra['startTime'] as int;
final duration = DateTime.now().millisecondsSinceEpoch - startTime;
final log = RequestLog(
requestId: requestId,
method: response.requestOptions.method,
url: response.requestOptions.uri.toString(),
params: response.requestOptions.queryParameters,
data: response.requestOptions.data,
headers: response.requestOptions.headers,
statusCode: response.statusCode,
responseData: response.data,
duration: duration,
networkType: _getNetworkType(),
requestTime: DateTime.fromMillisecondsSinceEpoch(startTime),
);
_reportLog(log);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final requestId = err.requestOptions.extra['requestId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString();
final startTime = err.requestOptions.extra['startTime'] as int? ?? DateTime.now().millisecondsSinceEpoch;
final duration = DateTime.now().millisecondsSinceEpoch - startTime;
final log = RequestLog(
requestId: requestId,
method: err.requestOptions.method,
url: err.requestOptions.uri.toString(),
params: err.requestOptions.queryParameters,
data: err.requestOptions.data,
headers: err.requestOptions.headers,
statusCode: err.response?.statusCode,
responseData: err.response?.data,
errorMsg: err.message,
errorStack: err.stackTrace?.toString(),
duration: duration,
networkType: _getNetworkType(),
requestTime: DateTime.fromMillisecondsSinceEpoch(startTime),
);
_reportLog(log);
handler.next(err);
}
}
步骤2:在NetworkUtil中添加日志拦截器
void _initDio() {
_dio = Dio();
// ... 其他基础配置
// 添加日志监控拦截器(建议放在最外层,确保能捕获所有请求/响应/错误)
_dio.interceptors.add(LogMonitorInterceptor());
// ... 其他拦截器(加密、缓存等)
}
2. 常见网络请求问题排查手册
整理10类高频网络请求问题,包含现象、原因与解决方案:
问题1:请求报“Connection refused”(连接被拒绝)
-
现象:开发环境下请求失败,错误信息含“Connection refused”;
-
原因:后端服务未启动、后端接口地址/端口错误、设备与后端服务不在同一网络(如本地后端,手机用流量);
-
解决方案:① 确认后端服务已启动;② 检查baseUrl是否正确(如本地后端应为“http://本地IP:端口”,而非“http://localhost:端口”);③ 确保设备与后端在同一WiFi网络。
问题2:HTTPS请求报“CERTIFICATE_VERIFY_FAILED”(证书验证失败)
-
现象:HTTPS请求失败,错误信息含“CERTIFICATE_VERIFY_FAILED”;
-
原因:后端证书无效(如自签名证书)、证书过期、证书域名与请求域名不匹配、证书pinning配置错误;
-
解决方案:① 开发环境可临时禁用证书验证(不推荐生产);② 生产环境确保后端使用正规CA颁发的证书;③ 检查证书pinning的指纹是否正确;④ 确认请求域名与证书绑定域名一致。
问题3:请求超时(TimeoutException)
-
现象:请求长时间无响应,最终报超时错误;
-
原因:网络环境差(弱网)、后端接口处理耗时过长、超时时间设置过短、大文件上传下载未延长超时;
-
解决方案:① 弱网环境延长超时时间(参考前文弱网适配);② 优化后端接口性能;③ 大文件传输单独设置长超时;④ 实现请求重试机制;⑤ 显示加载超时提示与重试按钮。
问题4:JSON解析失败(FormatException: Unexpected character)
-
现象:请求成功,但解析JSON时失败;
-
原因:后端返回非JSON格式数据(如HTML错误页)、返回JSON格式不规范(如逗号多余)、加密/解密后数据格式错误;
-
解决方案:① 查看请求日志中的responseData,确认返回数据格式;② 检查后端接口是否正常(如是否返回500错误页);③ 检查加密/解密逻辑,确保解密后为标准JSON;④ 解析前先判断数据格式。
问题5:401未授权(Unauthorized)
-
现象:请求返回401状态码,提示未授权;
-
原因:token过期/无效、token未正确添加到请求头、后端token验证逻辑错误;
-
解决方案:① 检查token是否存在且未过期(如本地存储的token是否有效);② 查看请求日志,确认Authorization头是否正确添加;③ 实现token过期自动刷新逻辑;④ 未授权时跳转登录页。
问题6:403禁止访问(Forbidden)
-
现象:请求返回403状态码,提示禁止访问;
-
原因:用户无该接口权限、IP被限制、请求头缺失必要参数(如Referer);
-
解决方案:① 确认用户是否有接口访问权限;② 检查IP是否被后端限制;③ 查看后端接口文档,确认是否缺失必要请求头;④ 联系后端排查权限配置。
问题7:404接口不存在(Not Found)
-
现象:请求返回404状态码,提示接口不存在;
-
原因:接口路径错误、baseUrl配置错误、后端接口未部署/已删除;
-
解决方案:① 检查请求URL是否正确(拼接baseUrl后是否与后端一致);② 确认环境配置是否正确(如开发环境用了生产baseUrl);③ 联系后端确认接口是否存在且部署成功。
问题8:500服务器内部错误(Internal Server Error)
-
现象:请求返回500状态码,提示服务器错误;
-
原因:后端接口代码异常、数据库错误、请求参数不符合后端预期;
-
解决方案:① 查看请求日志,确认请求参数是否正确;② 联系后端查看服务器日志,定位接口异常原因;③ 前端对请求参数做合法性校验,避免传入非法参数。
问题9:上传大文件失败(请求被中断/超时)
-
现象:上传大文件时请求中断或超时;
-
原因:上传超时时间过短、后端限制文件大小、网络不稳定;
-
解决方案:① 延长上传超时时间;② 实现分块上传(将大文件分成多个小片段上传);③ 显示上传进度,提升用户感知;④ 弱网环境下暂停/继续上传。
问题10:缓存数据未更新(获取到旧数据)
-
现象:数据已更新,但请求仍返回旧数据;
-
原因:缓存未失效、缓存key生成不唯一、数据更新后未主动清理缓存;
-
解决方案:① 检查缓存有效期设置是否合理;② 确认缓存key是否唯一(包含请求方法、URL、参数);③ 数据更新后主动调用invalidateCache清理对应缓存;④ 关键数据使用不缓存或短缓存策略。
四、结语:构建全链路可靠的网络交互体系
Flutter网络请求的优化与问题排查,是保障应用稳定性与用户体验的关键环节。本文从“提升体验”的缓存策略、“适配极端环境”的弱网/断网方案,到“降低排障成本”的监控与问题排查手册,形成了覆盖“请求前-请求中-请求后-问题解决”的全链路实战体系。
实际开发中,需结合业务场景灵活选择技术方案:非实时数据优先添加缓存,提交类操作实现离线队列,线上环境必须接入请求监控,同时提前梳理常见问题的解决方案。通过本文的实战技巧,开发者可快速解决网络请求中的各类痛点,构建“高可用、易维护、优体验”的Flutter网络交互体系,为应用的商业化落地提供坚实支撑。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐

所有评论(0)