第十四讲 网络请求与数据解析

前言:

这一章是比较重要的交互,实际上是比较通用的代码,就是进行http请求和本地缓存的一些库,与后端交互或者纯app请求一些tts、转录之类的模型,都是需要的,跑不掉。

一、总览

本讲聚焦Flutter开发中网络通信数据处理的核心能力,解决以下关键问题:

  • 统一管理网络请求(GET/POST),避免重复代码
  • 处理请求头、Token认证、超时、异常等网络边界场景
  • 规范化JSON数据解析,避免手动解析的错误
  • 实现网络缓存、离线可用等提升用户体验的策略
  • 通过Dio封装和拦截器实现请求/响应的全局管控

让你的Flutter应用能稳定、高效、安全地与后端交互,并优雅处理数据

  1. 分层设计:业务层不直接接触原生网络层,通过封装的Dio工具类解耦
  2. 拦截器核心:请求发出/响应返回时的"中间件",统一处理通用逻辑
  3. 数据流转:网络响应→拦截器处理→JSON解析→模型类→业务层(可结合缓存)
  4. 异常闭环:所有网络错误在拦截器/工具类中统一捕获,避免业务层零散处理

二、核心技术拆解

1. Dio封装与拦截器

添加Dio依赖

1.1 核心属性说明
属性/方法 作用 常用值/场景
BaseOptions Dio基础配置 baseUrl(接口前缀)、connectTimeout(连接超时)、headers(默认头)
interceptors 拦截器列表 RequestInterceptor(请求拦截)、ResponseInterceptor(响应拦截)
get/post 请求方法 queryParameters(URL参数)、data(POST请求体)
DioException 异常类型 connectionTimeout(超时)、badResponse(服务端错误)
1.2 基础封装
dart 复制代码
import 'dart:math';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

/// 全局Dio封装类
class HttpManager {
  static final HttpManager _instance = HttpManager._internal();
  factory HttpManager() => _instance;
  
  late Dio _dio;
  
  // 私有构造函数
  HttpManager._internal() {
    // 初始化Dio配置
    BaseOptions options = BaseOptions(
      baseUrl: "https://api.example.com", // 基础地址
      connectTimeout: const Duration(seconds: 10), // 连接超时
      receiveTimeout: const Duration(seconds: 5), // 接收超时
      headers: {
        "Content-Type": "application/json",
        "version": "1.0.0"
      }, // 默认请求头
    );
    
    _dio = Dio(options);
    
    // 添加拦截器
    _addInterceptors();
  }
  
  // 添加拦截器
  void _addInterceptors() {
    // 1. 请求拦截器:添加Token、日志
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
        // 添加Token
        String? token = _getToken();
        if (token != null) {
          options.headers["Authorization"] = "Bearer $token";
        }
        
        // 开发环境打印请求日志
        if (kDebugMode) {
          print("请求URL:${options.uri}");
          print("请求参数:${options.data}");
        }
        
        handler.next(options); // 继续执行
      },
      
      // 2. 响应拦截器:统一处理响应、错误
      onResponse: (Response response, ResponseInterceptorHandler handler) {
        if (kDebugMode) {
          print("响应数据:${response.data}");
        }
        handler.next(response);
      },
      
      // 3. 错误拦截器:统一处理异常
      onError: (DioException e, ErrorInterceptorHandler handler) {
        if (kDebugMode) {
          print("请求错误:${e.message}");
        }
        
        // 错误处理逻辑(重试/提示)
        _handleError(e, handler);
      },
    ));
    
    // 可选:添加日志拦截器(更详细)
    if (kDebugMode) {
      _dio.interceptors.add(LogInterceptor(responseBody: true));
    }
  }
  
  // 获取本地Token(示例)
  String? _getToken() {
    // 实际项目中从SharedPreferences等存储中获取
    return "your_token_here";
  }
  
  // 错误处理与重试
  void _handleError(DioException e, ErrorInterceptorHandler handler) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.receiveTimeout:
        // 超时错误,可实现重试逻辑
        _retryRequest(e.requestOptions, handler);
        break;
      case DioExceptionType.badResponse:
        // 服务器错误(4xx/5xx)
        if (e.response?.statusCode == 401) {
          // Token过期,跳转登录页
          // Navigator.pushReplacementNamed(context, "/login");
        }
        handler.next(e);
        break;
      default:
        handler.next(e);
    }
  }
  
  // 重试请求
  void _retryRequest(RequestOptions options, ErrorInterceptorHandler handler) {
    // 简单重试逻辑:最多重试1次
    int retryCount = 0;
    if (retryCount < 1) {
      retryCount++;
      _dio.request(options.path, data: options.data).then((response) {
        handler.resolve(response);
        retryCount = 0;
      }).catchError((error) {
        handler.next(error as DioException);
        retryCount = 0;
      });
    } else {
      retryCount = 0;
    }
  }
  
  // 暴露GET请求方法
  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? params,
    Options? options,
  }) async {
    try {
      Response response = await _dio.get(path, queryParameters: params, options: options);
      return response.data as T;
    } on DioException catch (e) {
      throw _convertError(e);
    }
  }
  
  // 暴露POST请求方法
  Future<T> post<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? params,
    Options? options,
  }) async {
    try {
      Response response = await _dio.post(
        path,
        data: data,
        queryParameters: params,
        options: options,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw _convertError(e);
    }
  }
  
  // 统一错误转换
  String _convertError(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        return "网络连接超时,请检查网络";
      case DioExceptionType.receiveTimeout:
        return "数据接收超时";
      case DioExceptionType.badResponse:
        return "服务器错误(${e.response?.statusCode})";
      case DioExceptionType.connectionError:
        return "网络连接失败,请检查网络";
      default:
        return e.message ?? "未知错误";
    }
  }
  
  // 获取原始Dio实例(特殊场景使用)
  Dio get dio => _dio;
}
1.3 注意事项
  • 拦截器中必须调用handler.next()/handler.resolve()/handler.reject(),否则请求会卡住
  • Token过期处理需结合路由(需确保上下文可用,或使用全局状态管理)
  • 重试逻辑要限制次数,避免无限重试
  • 生产环境需关闭日志拦截器,防止敏感信息泄露

2. JSON序列化(json_serializable)

(1)配置依赖(pubspec.yaml)
yaml 复制代码
dependencies:
  json_annotation: ^4.11.0

dev_dependencies:
  json_serializable: ^6.9.0
  build_runner: ^2.4.15
(2)模型类案例
dart 复制代码
import 'package:json_annotation/json_annotation.dart';

// 生成的文件命名:当前文件命.g.dart
part 'user_model.g.dart';

/// 用户模型类
@JsonSerializable()
class UserModel {
  // 字段名与JSON字段一致(不一致可通过@JsonKey指定)
  final int id;
  final String name;
  final String email;
  
  // 可选字段(JSON中可能不存在)
  @JsonKey(defaultValue: "")
  final String avatar;
  
  // 自定义字段映射(JSON字段是create_time,模型中是createTime)
  @JsonKey(name: "create_time")
  final String createTime;
  
  UserModel({
    required this.id,
    required this.name,
    required this.email,
    required this.avatar,
    required this.createTime,
  });
  
  // 从JSON解析为模型(自动生成)
  factory UserModel.fromJson(Map<String, dynamic> json) => _$UserModelFromJson(json);
  
  // 模型转换为JSON(自动生成)
  Map<String, dynamic> toJson() => _$UserModelToJson(this);
}
(3)生成序列化代码

执行终端命令:

arduino 复制代码
flutter pub run build_runner build
# 或监听文件变化自动生成
flutter pub run build_runner watch
(4)注意事项
  • 模型类必须添加@JsonSerializable()注解
  • part 'xxx.g.dart'必须与文件名一致
  • 可选字段需通过@JsonKey(defaultValue: ...)指定默认值,避免解析空值报错
  • 嵌套模型类也需要添加注解并实现序列化方法

3. 缓存与离线策略

(1)核心思路
  • 缓存网络响应数据到本地(SharedPreferences/Hive)
  • 请求时先读缓存(展示旧数据),再请求网络(更新数据)
  • 可设置缓存过期时间,避免展示过期数据
(2)缓存工具类案例
dart 复制代码
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class CacheManager {
  static final CacheManager _instance = CacheManager._internal();
  factory CacheManager() => _instance;
  CacheManager._internal();
  
  late SharedPreferences _prefs;
  
  // 初始化
  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
  }
  
  // 保存缓存(带过期时间,单位:秒)
  Future<void> setCache(String key, dynamic data, {int expireSeconds = 300}) async {
    final cacheData = {
      "data": data,
      "timestamp": DateTime.now().millisecondsSinceEpoch,
      "expire": expireSeconds * 1000
    };
    await _prefs.setString(key, json.encode(cacheData));
  }
  
  // 获取缓存
  dynamic getCache(String key) {
    String? cacheStr = _prefs.getString(key);
    if (cacheStr == null) return null;
    
    Map<String, dynamic> cacheData = json.decode(cacheStr);
    int timestamp = cacheData["timestamp"];
    int expire = cacheData["expire"];
    
    // 检查是否过期
    if (DateTime.now().millisecondsSinceEpoch - timestamp > expire) {
      removeCache(key); // 删除过期缓存
      return null;
    }
    
    return cacheData["data"];
  }
  
  // 删除缓存
  Future<void> removeCache(String key) async {
    await _prefs.remove(key);
  }
  
  // 清空所有缓存
  Future<void> clearAllCache() async {
    await _prefs.clear();
  }
}

三、综合应用案例

需求场景

实现一个"用户信息"模块,包含:

  1. 获取用户列表(GET请求,带Token)
  2. 提交用户信息(POST请求)
  3. 数据序列化到模型类
  4. 异常处理、超时重试
  5. 离线缓存用户列表

步骤1:初始化工具类

csharp 复制代码
// 在main.dart中初始化
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化缓存
  await CacheManager().init();
  
  runApp(const MyApp());
}

步骤2:定义API接口类

dart 复制代码
/// 业务API封装
class UserApi {
  static final HttpManager _http = HttpManager();
  static final CacheManager _cache = CacheManager();
  
  // 1. 获取用户列表(带缓存)
  Future<List<UserModel>> getUserList({int page = 1, int size = 10}) async {
    String cacheKey = "user_list_$page";
    
    // 1. 先读缓存(离线展示)
    dynamic cacheData = _cache.getCache(cacheKey);
    if (cacheData != null) {
      // 缓存数据转换为模型
      List<UserModel> cacheList = (cacheData as List)
          .map((e) => UserModel.fromJson(e))
          .toList();
      // 先返回缓存,再异步请求更新
      Future.delayed(Duration.zero, () => _fetchUserList(page, size));
      return cacheList;
    }
    
    // 2. 无缓存则请求网络
    return _fetchUserList(page, size);
  }
  
  // 实际请求用户列表
  Future<List<UserModel>> _fetchUserList(int page, int size) async {
    String cacheKey = "user_list_$page";
    try {
      // GET请求
      Map<String, dynamic> response = await _http.get(
        "/users",
        params: {"page": page, "size": size},
      );
      
      // 解析为模型列表
      List<UserModel> userList = (response["data"] as List)
          .map((e) => UserModel.fromJson(e))
          .toList();
      
      // 保存缓存(5分钟过期)
      _cache.setCache(
        cacheKey,
        response["data"],
        expireSeconds: 300,
      );
      
      return userList;
    } catch (e) {
      throw e.toString();
    }
  }
  
  // 2. 提交用户信息(POST请求)
  Future<bool> submitUserInfo(UserModel user) async {
    try {
      // POST请求:模型转JSON
      Map<String, dynamic> response = await _http.post(
        "/users/save",
        data: user.toJson(),
      );
      
      return response["success"] == true;
    } catch (e) {
      throw e.toString();
    }
  }
}

步骤3:业务页面使用

php 复制代码
import 'package:flutter/material.dart';

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

  @override
  State<UserPage> createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  List<UserModel> _userList = [];
  bool _isLoading = true;
  String? _errorMsg;
  
  @override
  void initState() {
    super.initState();
    _loadUserList();
  }
  
  // 加载用户列表
  Future<void> _loadUserList() async {
    setState(() {
      _isLoading = true;
      _errorMsg = null;
    });
    
    try {
      List<UserModel> list = await UserApi().getUserList(page: 1);
      setState(() {
        _userList = list;
      });
    } catch (e) {
      setState(() {
        _errorMsg = e.toString();
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
  
  // 提交用户信息示例
  Future<void> _submitUser() async {
    UserModel newUser = UserModel(
      id: 0, // 新增用户ID为0
      name: "张三",
      email: "zhangsan@example.com",
      avatar: "",
      createTime: DateTime.now().toString(),
    );
    
    try {
      bool success = await UserApi().submitUserInfo(newUser);
      if (success) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("提交成功")),
        );
        _loadUserList(); // 重新加载列表
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("提交失败:$e"), backgroundColor: Colors.red),
      );
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("用户管理")),
      body: _buildBody(),
      floatingActionButton: FloatingActionButton(
        onPressed: _submitUser,
        child: const Icon(Icons.add),
      ),
    );
  }
  
  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }
    
    if (_errorMsg != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_errorMsg!),
            ElevatedButton(
              onPressed: _loadUserList,
              child: const Text("重新加载"),
            ),
          ],
        ),
      );
    }
    
    return ListView.builder(
      itemCount: _userList.length,
      itemBuilder: (context, index) {
        UserModel user = _userList[index];
        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
          trailing: Text(user.createTime),
        );
      },
    );
  }
}

核心关键点

  1. Dio封装:通过单例模式统一管理Dio配置,拦截器处理Token、日志、异常、重试等通用逻辑
  2. JSON序列化 :使用json_serializable自动生成解析代码,避免手动解析的错误,通过@JsonKey处理字段映射
  3. 完整链路:业务层→封装的Http工具→拦截器→网络请求→数据解析→缓存→业务层,实现请求-解析-缓存的闭环
  4. 离线策略:先读缓存展示数据,再异步请求更新,提升用户体验,同时设置缓存过期时间保证数据新鲜度

工程化建议

  • 所有API接口统一放在单独的api目录,按业务模块拆分
  • 模型类放在models目录,与API一一对应
  • 网络错误提示统一封装为Toast/SnackBar,避免重复代码
  • 生产环境关闭日志拦截器,添加接口加密/签名(可选)
相关推荐
不甜情歌2 小时前
JS 拷贝:浅拷贝 / 深拷贝原理 + 常用方法
前端·javascript
敲代码的约德尔人2 小时前
Vue 3 响应式系统完全指南:我在 4 个项目中踩坑后总结的血泪经验
前端·vue.js
Roselind_Yi2 小时前
技术拆解:《从音频到动效:我是如何用 Web Audio API 拆解音乐的?》
前端·javascript·人工智能·音视频·语音识别·实时音视频·audiolm
和科比合砍81分2 小时前
pnpm:public-hoist-pattern[]配置
前端
我叫黑大帅2 小时前
Js常用数组处理
前端·javascript·面试
敲代码的约德尔人2 小时前
React useEffect 完全指南:我在 3 个项目中踩坑后总结的血泪经验
前端·react.js
小凡同志2 小时前
React 组件设计模式:从 HOC 到 Render Props 再到 Hooks
前端·react.js
毛骗导演2 小时前
OpenClaw Auth Profile 与多 Key 冷却隔离机制深度解析:一个 API Key 是如何被选择、追踪并轮换的
前端·架构
用户9751470751362 小时前
如何在 Vite 中配置 CSS 模块,以避免全局样式被模块化隔离覆盖?
前端