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网络交互体系,为应用的商业化落地提供坚实支撑。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐