详细介绍:【第四阶段-数据处理与网络】第二章:网络请求—就像发送邮件

什么是网络请求?

在这里插入图片描述

想象一下网络请求就像是:

  • 发送邮件:你写好信件(请求数据),投递到邮箱(发送到服务器)
  • 等待回复:邮递员(网络)将信件送达,对方回信(服务器响应)
  • 收到回复:你收到回信(获得响应数据),根据内容采取行动

在Flutter中,网络请求让我们的应用能够与远程服务器交换数据,获取最新信息或提交用户数据。


一:HTTP基础 - 网络通信的语言

1.1 基础概念

HTTP就像是网络世界的通用语言,定义了客户端和服务器之间如何交流。

首先,在pubspec.yaml中添加依赖:

dependencies:
http: ^1.1.0

1.2 基础HTTP请求示例

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;  // 导入http库,使用别名http
import 'dart:convert';  // 导入JSON编解码库
void main() => runApp(MaterialApp(home: HttpDemo()));
/// HTTP网络请求演示页面
class HttpDemo extends StatefulWidget {

_HttpDemoState createState() => _HttpDemoState();
}
class _HttpDemoState extends State<HttpDemo> {
  // 加载状态标志,用于显示加载动画和禁用按钮
  bool _isLoading = false;
  // 存储服务器响应的数据,用于显示在界面上
  String _responseData = '';
  // 存储帖子列表数据,dynamic类型可以存储任意JSON数据
  List<dynamic> _posts = [];
    
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(title: Text('HTTP 网络请求演示')),
    body: Padding(
    padding: EdgeInsets.all(16),
    child: Column(
    children: [
    // HTTP方法按钮
    Row(
    children: [
    Expanded(
    child: ElevatedButton(
    onPressed: _isLoading ? null : _performGetRequest,
    child: Text('GET'),
    ),
    ),
    SizedBox(width: 8),
    Expanded(
    child: ElevatedButton(
    onPressed: _isLoading ? null : _performPostRequest,
    child: Text('POST'),
    ),
    ),
    ],
    ),
    SizedBox(height: 16),
    // 响应数据显示
    if (_isLoading)
    CircularProgressIndicator()
    else if (_responseData.isNotEmpty)
    Expanded(
    child: Container(
    width: double.infinity,
    padding: EdgeInsets.all(12),
    decoration: BoxDecoration(
    color: Colors.grey[200],
    borderRadius: BorderRadius.circular(8),
    ),
    child: SingleChildScrollView(
    child: Text(_responseData, style: TextStyle(fontSize: 12)),
    ),
    ),
    ),
    SizedBox(height: 16),
    // 帖子列表
    ElevatedButton(
    onPressed: _isLoading ? null : _loadPosts,
    child: Text('加载帖子列表'),
    ),
    if (_posts.isNotEmpty)
    Expanded(
    child: ListView.builder(
    itemCount: _posts.length > 5 ? 5 : _posts.length,
    itemBuilder: (context, index) {
    final post = _posts[index];
    return ListTile(
    title: Text(post['title']),
    subtitle: Text(post['body'], maxLines: 2),
    );
    },
    ),
    ),
    ],
    ),
    ),
    );
    }
    /// GET请求示例
    /// GET用于从服务器获取数据,不会修改服务器数据
    Future<void> _performGetRequest() async {
      // 更新UI状态:开始加载,清空之前的响应数据
      setState(() {
      _isLoading = true;
      _responseData = '';
      });
      try {
      // 发送GET请求
      // Uri.parse: 将字符串URL转换为Uri对象
      // await: 等待异步操作完成
      final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
      headers: {'Content-Type': 'application/json'},  // 设置请求头,告诉服务器我们发送/接收JSON格式
      );
      // 检查HTTP状态码
      // 200表示成功,其他状态码表示不同的错误情况
      if (response.statusCode == 200) {
      // json.decode: 将JSON字符串解析为Dart对象(Map或List)
      final data = json.decode(response.body);
      setState(() {
      // JsonEncoder.withIndent: 格式化JSON输出,使其更易读
      // '  '表示使用2个空格作为缩进
      _responseData = JsonEncoder.withIndent('  ').convert(data);
      });
      } else {
      // 如果状态码不是200,抛出异常
      throw Exception('请求失败: ${response.statusCode}');
      }
      } catch (e) {
      // 捕获所有异常(网络错误、解析错误等)
      setState(() => _responseData = '请求失败: $e');
      } finally {
      // finally块总是会执行,无论是否发生异常
      // 用于清理工作,这里用于停止加载动画
      setState(() => _isLoading = false);
      }
      }
      /// POST请求示例
      /// POST用于向服务器提交数据,通常用于创建新资源
      Future<void> _performPostRequest() async {
        setState(() {
        _isLoading = true;
        _responseData = '';
        });
        try {
        // 准备要发送的数据
        // 这是一个Map对象,将被转换为JSON格式发送
        final requestData = {
        'title': 'Flutter学习笔记',
        'body': '这是一个POST请求示例',
        'userId': 1,
        };
        // 发送POST请求
        final response = await http.post(
        Uri.parse('https://jsonplaceholder.typicode.com/posts'),
        headers: {'Content-Type': 'application/json'},
        // json.encode: 将Dart对象转换为JSON字符串
        // POST请求需要将数据放在body中发送
        body: json.encode(requestData),
        );
        // 201 Created: 表示成功创建了新资源
        // 这是POST请求成功的标准状态码
        if (response.statusCode == 201) {
        final data = json.decode(response.body);
        setState(() {
        _responseData = JsonEncoder.withIndent('  ').convert(data);
        });
        } else {
        throw Exception('请求失败: ${response.statusCode}');
        }
        } catch (e) {
        setState(() => _responseData = '请求失败: $e');
        } finally {
        setState(() => _isLoading = false);
        }
        }
        /// 加载帖子列表示例
        /// 演示如何获取并显示列表数据
        Future<void> _loadPosts() async {
          setState(() => _isLoading = true);
          try {
          // 获取帖子列表
          // 这个API返回的是一个JSON数组
          final response = await http.get(
          Uri.parse('https://jsonplaceholder.typicode.com/posts'),
          );
          if (response.statusCode == 200) {
          // json.decode会将JSON数组解析为List<dynamic>
            // 每个元素是一个Map<String, dynamic>
              setState(() => _posts = json.decode(response.body));
              }
              } catch (e) {
              // 这里使用print输出错误,实际项目中应该显示给用户
              print('加载失败: $e');
              } finally {
              setState(() => _isLoading = false);
              }
              }
              }

核心知识点:

  • http.get(): 发送GET请求
  • http.post(): 发送POST请求
  • Uri.parse(): 解析URL
  • json.encode() / json.decode(): JSON编码/解码
  • headers: 设置请求头
  • response.statusCode: 响应状态码
  • response.body: 响应体

二:Dio高级网络库

2.1 基础概念

Dio是一个功能更强大的网络库,提供拦截器、并发请求、文件上传下载等高级功能。

首先添加依赖:

dependencies:
dio: ^5.3.2

2.2 Dio使用示例

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
/// 网络服务类 - 使用单例模式
/// 单例模式确保整个应用只有一个ApiService实例
/// 这样可以共享配置和拦截器,避免重复创建
class ApiService {
// 静态私有实例变量,保存唯一的ApiService实例
static final ApiService _instance = ApiService._internal();
// 工厂构造函数,总是返回同一个实例
factory ApiService() => _instance;
// 私有命名构造函数,防止外部直接创建实例
ApiService._internal();
// Dio实例,late表示延迟初始化
late Dio _dio;
/// 初始化Dio配置
void initialize() {
// 创建Dio实例并配置基础选项
_dio = Dio(BaseOptions(
// 基础URL,所有请求都会以此为前缀
baseUrl: 'https://jsonplaceholder.typicode.com',
// 连接超时时间:建立连接的最长等待时间
connectTimeout: Duration(seconds: 10),
// 接收超时时间:接收数据的最长等待时间
receiveTimeout: Duration(seconds: 10),
// 默认请求头,所有请求都会带上这些头
headers: {'Content-Type': 'application/json'},
));
// 添加拦截器 - 可以在请求/响应前后执行自定义逻辑
_dio.interceptors.add(InterceptorsWrapper(
// 请求拦截器:在请求发送前执行
onRequest: (options, handler) {
// 打印请求信息,方便调试
print('请求: ${options.method} ${options.path}');
// 可以在这里添加token等认证信息
// options.headers['Authorization'] = 'Bearer $token';
// 继续执行请求
handler.next(options);
},
// 响应拦截器:在收到响应后执行
onResponse: (response, handler) {
// 打印响应状态码
print('响应: ${response.statusCode}');
// 可以在这里处理通用响应逻辑
// 继续传递响应
handler.next(response);
},
// 错误拦截器:当请求发生错误时执行
onError: (error, handler) {
// 打印错误信息
print('错误: ${error.message}');
// 可以在这里统一处理错误(如401未授权跳转登录)
// 继续传递错误
handler.next(error);
},
));
}
/// 发送GET请求
/// [path] 请求路径,会自动拼接baseUrl
/// [params] 查询参数,会被转换为URL参数 (?key=value)
Future<Response> get(String path, {Map<String, dynamic>? params}) async {
  return await _dio.get(path, queryParameters: params);
  }
  /// 发送POST请求
  /// [path] 请求路径
  /// [data] 请求体数据,会自动转换为JSON
  Future<Response> post(String path, {dynamic data}) async {
    return await _dio.post(path, data: data);
    }
    }
    // Dio演示页面
    class DioDemo extends StatefulWidget {
    
    _DioDemoState createState() => _DioDemoState();
    }
    class _DioDemoState extends State<DioDemo> {
      final _apiService = ApiService();
      bool _isLoading = false;
      String _responseData = '';
      
      void initState() {
      super.initState();
      _apiService.initialize();
      }
      
      Widget build(BuildContext context) {
      return Scaffold(
      appBar: AppBar(title: Text('Dio 网络请求')),
      body: Padding(
      padding: EdgeInsets.all(16),
      child: Column(
      children: [
      Row(
      children: [
      Expanded(
      child: ElevatedButton(
      onPressed: _isLoading ? null : _dioGetRequest,
      child: Text('GET请求'),
      ),
      ),
      SizedBox(width: 8),
      Expanded(
      child: ElevatedButton(
      onPressed: _isLoading ? null : _dioPostRequest,
      child: Text('POST请求'),
      ),
      ),
      ],
      ),
      SizedBox(height: 16),
      Row(
      children: [
      Expanded(
      child: ElevatedButton(
      onPressed: _isLoading ? null : _dioConcurrentRequests,
      child: Text('并发请求'),
      ),
      ),
      SizedBox(width: 8),
      Expanded(
      child: ElevatedButton(
      onPressed: _isLoading ? null : _dioWithParams,
      child: Text('带参数'),
      ),
      ),
      ],
      ),
      SizedBox(height: 16),
      if (_isLoading)
      CircularProgressIndicator()
      else if (_responseData.isNotEmpty)
      Expanded(
      child: Container(
      width: double.infinity,
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
      color: Colors.grey[200],
      borderRadius: BorderRadius.circular(8),
      ),
      child: SingleChildScrollView(
      child: Text(_responseData, style: TextStyle(fontSize: 12)),
      ),
      ),
      ),
      ],
      ),
      ),
      );
      }
      // Dio GET请求
      Future<void> _dioGetRequest() async {
        setState(() {
        _isLoading = true;
        _responseData = '';
        });
        try {
        final response = await _apiService.get('/posts/1');
        setState(() {
        _responseData = JsonEncoder.withIndent('  ').convert(response.data);
        });
        } catch (e) {
        setState(() => _responseData = '请求失败: $e');
        } finally {
        setState(() => _isLoading = false);
        }
        }
        // Dio POST请求
        Future<void> _dioPostRequest() async {
          setState(() {
          _isLoading = true;
          _responseData = '';
          });
          try {
          final response = await _apiService.post('/posts', data: {
          'title': 'Dio POST示例',
          'body': '这是Dio POST请求',
          'userId': 1,
          });
          setState(() {
          _responseData = JsonEncoder.withIndent('  ').convert(response.data);
          });
          } catch (e) {
          setState(() => _responseData = '请求失败: $e');
          } finally {
          setState(() => _isLoading = false);
          }
          }
          /// 并发请求示例
          /// 演示如何同时发送多个请求并等待所有结果
          Future<void> _dioConcurrentRequests() async {
            setState(() {
            _isLoading = true;
            _responseData = '';
            });
            try {
            // 创建多个Future对象,每个代表一个异步请求
            // 这些请求会并行执行,而不是顺序执行
            final futures = [
            _apiService.get('/posts/1'),
            _apiService.get('/posts/2'),
            _apiService.get('/posts/3'),
            ];
            // Future.wait: 等待所有Future完成
            // 如果任何一个失败,整个操作就会失败
            final responses = await Future.wait(futures);
            // 提取所有响应的数据部分
            final results = responses.map((r) => r.data).toList();
            setState(() {
            //JsonEncoder.withIndent('  ')将多个响应结果格式化为美观的JSON字符串,便于查看.(两个空格)表示每一层嵌套使用两个空格作为缩进
            _responseData = JsonEncoder.withIndent('  ').convert(results);
            });
            } catch (e) {
            setState(() => _responseData = '并发请求失败: $e');
            } finally {
            setState(() => _isLoading = false);
            }
            }
            /// 带查询参数的请求示例
            /// 演示如何在GET请求中传递参数
            Future<void> _dioWithParams() async {
              setState(() {
              _isLoading = true;
              _responseData = '';
              });
              //JsonEncoder.withIndent('  ')将多个响应结果格式化为美观的JSON字符串,便于查看.(两个空格)表示每一层嵌套使用两个空格作为缩进
              try {
              // 查询参数会被转换为URL参数
              // 最终URL: /posts?userId=1&_limit=3
              final response = await _apiService.get('/posts', params: {
              'userId': 1,      // 筛选userId为1的帖子
              '_limit': 3,      // 限制返回3条数据
              });
              setState(() {
              _responseData = JsonEncoder.withIndent('  ').convert(response.data);
              });
              } catch (e) {
              setState(() => _responseData = '请求失败: $e');
              } finally {
              setState(() => _isLoading = false);
              }
              }
              }

核心知识点:

  • Dio(): 创建Dio实例
  • BaseOptions: 配置基础选项(baseUrl、超时等)
  • interceptors: 拦截器(请求/响应/错误拦截)
  • queryParameters: 查询参数
  • Future.wait(): 并发请求

三:JSON数据处理

3.1 基础概念

JSON是一种轻量级的数据交换格式,需要在Dart对象和JSON字符串之间进行转换。

3.2 数据模型和JSON转换

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
/// 用户数据模型类
/// 将JSON数据映射为强类型的Dart对象,提供类型安全和代码提示
class User {
// final表示这些字段在创建后不可修改(不可变对象)
final int id;
final String name;
final String email;
// 构造函数,使用命名参数并标记为required(必需)
User({required this.id, required this.name, required this.email});
/// 工厂构造函数:从JSON Map创建User对象
/// 这个过程叫做"反序列化"或"解析"
/// [json] 是从服务器返回的JSON数据,已被json.decode解析为Map
factory User.fromJson(Map<String, dynamic> json) {
  return User(
  // 从Map中提取对应的字段值
  // json['id']会返回dynamic类型,Dart会自动转换为int
  id: json['id'],
  name: json['name'],
  email: json['email'],
  );
  }
  /// 将User对象转换为JSON Map
  /// 这个过程叫做"序列化"
  /// 返回的Map可以被json.encode转换为JSON字符串发送给服务器
  Map<String, dynamic> toJson() {
    return {
    'id': id,
    'name': name,
    'email': email,
    };
    }
    }
    // JSON处理演示
    class JsonDemo extends StatefulWidget {
    
    _JsonDemoState createState() => _JsonDemoState();
    }
    class _JsonDemoState extends State<JsonDemo> {
      List<User> _users = [];
        String _jsonString = '';
        bool _isLoading = false;
        
        Widget build(BuildContext context) {
        return Scaffold(
        appBar: AppBar(title: Text('JSON 数据处理')),
        body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
        children: [
        Row(
        children: [
        Expanded(
        child: ElevatedButton(
        onPressed: _isLoading ? null : _fetchAndParseJson,
        child: Text('获取JSON'),
        ),
        ),
        SizedBox(width: 8),
        Expanded(
        child: ElevatedButton(
        onPressed: _users.isEmpty ? null : _convertToJson,
        child: Text('转换JSON'),
        ),
        ),
        ],
        ),
        SizedBox(height: 16),
        // JSON字符串显示
        if (_jsonString.isNotEmpty)
        Container(
        height: 150,
        width: double.infinity,
        padding: EdgeInsets.all(12),
        decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(8),
        ),
        child: SingleChildScrollView(
        child: Text(_jsonString, style: TextStyle(fontSize: 12)),
        ),
        ),
        SizedBox(height: 16),
        // 用户列表
        if (_users.isNotEmpty)
        Expanded(
        child: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
        final user = _users[index];
        return ListTile(
        title: Text(user.name),
        subtitle: Text(user.email),
        leading: CircleAvatar(child: Text('${user.id}')),
        );
        },
        ),
        ),
        ],
        ),
        ),
        );
        }
        /// 获取并解析JSON数据
        /// 演示完整的JSON处理流程:请求 → 解析 → 转换为对象
        Future<void> _fetchAndParseJson() async {
          setState(() => _isLoading = true);
          try {
          // 1. 发送HTTP请求获取数据
          final response = await http.get(
          Uri.parse('https://jsonplaceholder.typicode.com/users'),
          );
          if (response.statusCode == 200) {
          // 2. 解析JSON字符串为Dart对象
          // response.body是字符串,json.decode将其解析为List<dynamic>
            // 因为这个API返回的是JSON数组
            final List<dynamic> jsonList = json.decode(response.body);
              // 3. 将动态类型的Map列表转换为强类型的User对象列表
              // map函数遍历每个元素,调用User.fromJson进行转换
              // toList()将Iterable转换为List
              final users = jsonList.map((json) => User.fromJson(json)).toList();
              setState(() {
              // 保存User对象列表,供UI使用
              _users = users;
              // 格式化JSON字符串用于显示
              _jsonString = JsonEncoder.withIndent('  ').convert(jsonList);
              });
              }
              } catch (e) {
              print('获取失败: $e');
              } finally {
              setState(() => _isLoading = false);
              }
              }
              /// 将对象转换为JSON
              /// 演示序列化过程:对象 → Map → JSON字符串
              void _convertToJson() {
              // 1. 将User对象列表转换为Map列表
              // 每个User对象调用toJson()方法转换为Map<String, dynamic>
                final jsonList = _users.map((user) => user.toJson()).toList();
                // 2. 将Map列表转换为格式化的JSON字符串
                // JsonEncoder.withIndent添加缩进,使输出更易读
                final jsonString = JsonEncoder.withIndent('  ').convert(jsonList);
                // 3. 更新UI显示
                setState(() => _jsonString = jsonString);
                }
                }

核心知识点:

  • fromJson(): 从JSON Map创建对象
  • toJson(): 将对象转换为JSON Map
  • json.decode(): 解析JSON字符串
  • json.encode(): 转换为JSON字符串
  • JsonEncoder.withIndent(): 格式化JSON输出
  • .map(): 批量转换列表

四:进阶知识点

4.1 HTTP状态码详解

理解HTTP状态码对于调试网络请求非常重要:

2xx 成功状态码:

  • 200 OK: GET请求成功
  • 201 Created: POST请求成功创建资源
  • 204 No Content: 请求成功但无返回内容(常用于DELETE)

3xx 重定向状态码:

  • 301 Moved Permanently: 永久重定向
  • 302 Found: 临时重定向
  • 304 Not Modified: 资源未修改,可使用缓存

4xx 客户端错误:

  • 400 Bad Request: 请求参数错误
  • 401 Unauthorized: 未授权,需要登录
  • 403 Forbidden: 禁止访问,权限不足
  • 404 Not Found: 资源不存在
  • 422 Unprocessable Entity: 请求格式正确但语义错误

5xx 服务器错误:

  • 500 Internal Server Error: 服务器内部错误
  • 502 Bad Gateway: 网关错误
  • 503 Service Unavailable: 服务不可用

4.2 错误处理最佳实践

/// 统一的网络请求错误处理
class NetworkException implements Exception {
final String message;
final int? statusCode;
NetworkException(this.message, {this.statusCode});

String toString() => 'NetworkException: $message (状态码: $statusCode)';
}
/// 封装的网络请求方法,包含完整的错误处理
Future<T> safeRequest<T>({
  required Future<Response> Function() request,
    required T Function(dynamic data) parser,
    }) async {
    try {
    final response = await request();
    // 根据状态码判断请求结果
    if (response.statusCode >= 200 && response.statusCode < 300) {
    return parser(response.data);
    } else if (response.statusCode == 401) {
    throw NetworkException('未授权,请先登录', statusCode: 401);
    } else if (response.statusCode == 404) {
    throw NetworkException('请求的资源不存在', statusCode: 404);
    } else if (response.statusCode >= 500) {
    throw NetworkException('服务器错误,请稍后重试', statusCode: response.statusCode);
    } else {
    throw NetworkException('请求失败', statusCode: response.statusCode);
    }
    } on DioException catch (e) {
    // 处理Dio特定的异常
    if (e.type == DioExceptionType.connectionTimeout) {
    throw NetworkException('连接超时,请检查网络');
    } else if (e.type == DioExceptionType.receiveTimeout) {
    throw NetworkException('接收数据超时');
    } else if (e.type == DioExceptionType.badResponse) {
    throw NetworkException('服务器响应错误', statusCode: e.response?.statusCode);
    } else {
    throw NetworkException('网络请求失败: ${e.message}');
    }
    } catch (e) {
    throw NetworkException('未知错误: $e');
    }
    }
    // 使用示例
    Future<List<User>> fetchUsers() async {
      return await safeRequest(
      request: () => ApiService().get('/users'),
      parser: (data) => (data as List).map((json) => User.fromJson(json)).toList(),
      );
      }

4.3 请求取消和超时控制

/// 支持取消的ApiService实现
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
late Dio _dio;
void initialize() {
_dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));
}
/// 支持CancelToken的GET请求
/// [path] 请求路径
/// [params] 查询参数
/// [cancelToken] 用于取消请求的token
Future<Response> get(
  String path, {
  Map<String, dynamic>? params,
    CancelToken? cancelToken,
    }) async {
    return await _dio.get(
    path,
    queryParameters: params,
    cancelToken: cancelToken,  // 传递CancelToken
    );
    }
    /// 支持CancelToken的POST请求
    Future<Response> post(
      String path, {
      dynamic data,
      CancelToken? cancelToken,
      }) async {
      return await _dio.post(
      path,
      data: data,
      cancelToken: cancelToken,
      );
      }
      }
      /// 使用CancelToken取消请求
      class RequestManager {
      final _cancelTokens = <String, CancelToken>{};
        /// 发送可取消的请求
        Future<Response> cancelableRequest(
          String key,
          Future<Response> Function(CancelToken) request,
            ) async {
            // 取消之前的同名请求
            _cancelTokens[key]?.cancel('新请求已发起');
            // 创建新的CancelToken
            final cancelToken = CancelToken();
            _cancelTokens[key] = cancelToken;
            try {
            final response = await request(cancelToken);
            _cancelTokens.remove(key);
            return response;
            } catch (e) {
            _cancelTokens.remove(key);
            rethrow;
            }
            }
            /// 取消指定请求
            void cancel(String key) {
            _cancelTokens[key]?.cancel('用户取消');
            _cancelTokens.remove(key);
            }
            /// 取消所有请求
            void cancelAll() {
            _cancelTokens.forEach((key, token) => token.cancel('取消所有请求'));
            _cancelTokens.clear();
            }
            }
            // 使用示例
            final requestManager = RequestManager();
            // 发送可取消的搜索请求
            Future<void> searchUsers(String keyword) async {
              try {
              final response = await requestManager.cancelableRequest(
              'search',
              (cancelToken) => ApiService().get(
              '/users',
              params: {'q': keyword},
              cancelToken: cancelToken,
              ),
              );
              // 处理响应...
              } catch (e) {
              if (e is DioException && CancelToken.isCancel(e)) {
              print('请求已取消');
              } else {
              print('请求失败: $e');
              }
              }
              }

4.4 请求重试机制

/// 带重试机制的请求拦截器
class RetryInterceptor extends Interceptor {
final int maxRetries;
final Duration retryDelay;
RetryInterceptor({
this.maxRetries = 3,
this.retryDelay = const Duration(seconds: 1),
});

void onError(DioException err, ErrorInterceptorHandler handler) async {
// 只对特定错误进行重试
if (_shouldRetry(err)) {
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;
if (retryCount < maxRetries) {
print('请求失败,${retryDelay.inSeconds}秒后进行第${retryCount + 1}次重试...');
// 等待一段时间后重试
await Future.delayed(retryDelay);
// 更新重试次数
err.requestOptions.extra['retryCount'] = retryCount + 1;
// 重新发送请求
try {
final response = await Dio().fetch(err.requestOptions);
handler.resolve(response);
return;
} catch (e) {
// 重试失败,继续错误处理
}
}
}
// 不重试或重试次数已用完
handler.next(err);
}
bool _shouldRetry(DioException err) {
// 只对网络错误和5xx服务器错误进行重试
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
(err.response?.statusCode ?? 0) >= 500;
}
}
// 使用示例
void initializeDioWithRetry() {
final dio = Dio();
dio.interceptors.add(RetryInterceptor(maxRetries: 3));
}

4.5 请求缓存策略

/// 简单的内存缓存实现
class CacheInterceptor extends Interceptor {
final _cache = <String, CacheEntry>{};
  final Duration cacheDuration;
  CacheInterceptor({this.cacheDuration = const Duration(minutes: 5)});
  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
  // 只缓存GET请求
  if (options.method == 'GET') {
  final key = _getCacheKey(options);
  final cached = _cache[key];
  // 如果缓存存在且未过期,直接返回缓存数据
  if (cached != null && !cached.isExpired) {
  print('使用缓存数据: $key');
  handler.resolve(cached.response);
  return;
  }
  }
  handler.next(options);
  }
  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
  // 缓存成功的GET请求响应
  if (response.requestOptions.method == 'GET' &&
  response.statusCode == 200) {
  final key = _getCacheKey(response.requestOptions);
  _cache[key] = CacheEntry(
  response: response,
  expireTime: DateTime.now().add(cacheDuration),
  );
  print('缓存数据: $key');
  }
  handler.next(response);
  }
  String _getCacheKey(RequestOptions options) {
  return '${options.uri}';
  }
  /// 清除所有缓存
  void clearCache() {
  _cache.clear();
  }
  /// 清除过期缓存
  void clearExpiredCache() {
  _cache.removeWhere((key, entry) => entry.isExpired);
  }
  }
  class CacheEntry {
  final Response response;
  final DateTime expireTime;
  CacheEntry({required this.response, required this.expireTime});
  bool get isExpired => DateTime.now().isAfter(expireTime);
  }

4.6 文件上传和下载

/// 文件上传示例
Future<void> uploadFile(String filePath) async {
  try {
  // 创建FormData
  final formData = FormData.fromMap({
  'file': await MultipartFile.fromFile(
  filePath,
  filename: filePath.split('/').last,
  ),
  'description': '文件描述',
  });
  // 发送上传请求,带进度回调
  final response = await Dio().post(
  'https://api.example.com/upload',
  data: formData,
  onSendProgress: (sent, total) {
  final progress = (sent / total * 100).toStringAsFixed(1);
  print('上传进度: $progress%');
  },
  );
  print('上传成功: ${response.data}');
  } catch (e) {
  print('上传失败: $e');
  }
  }
  /// 文件下载示例
  Future<void> downloadFile(String url, String savePath) async {
    try {
    await Dio().download(
    url,
    savePath,
    onReceiveProgress: (received, total) {
    if (total != -1) {
    final progress = (received / total * 100).toStringAsFixed(1);
    print('下载进度: $progress%');
    }
    },
    );
    print('下载完成: $savePath');
    } catch (e) {
    print('下载失败: $e');
    }
    }

学习总结

通过这一章的学习,你已经掌握了:

网络请求技术

特点适用场景
http简单轻量基础HTTP请求
Dio功能强大复杂网络需求

HTTP方法:

  • GET: 获取数据
  • POST: 创建数据
  • PUT: 更新数据
  • DELETE: 删除数据

️ 实践技能

  1. 发送请求: 使用http/Dio发送各种HTTP请求
  2. 处理响应: 解析响应数据和状态码
  3. 错误处理: 使用try-catch处理网络异常
  4. JSON转换: 在JSON和Dart对象之间转换

最佳实践

  1. 封装服务类: 将网络请求封装为服务类
  2. 统一错误处理: 使用拦截器统一处理错误
  3. 数据模型: 使用强类型模型而非Map
  4. 异步处理: 正确使用async/await
  5. 超时设置: 设置合理的超时时间
  6. 并发控制: 合理使用并发请求

常见问题

1. 如何处理超时?

Dio(BaseOptions(
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
));

2. 如何添加请求头?

headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
}

3. 如何处理错误?

try {
final response = await http.get(url);
if (response.statusCode == 200) {
// 成功处理
} else {
// 错误处理
}
} catch (e) {
// 异常处理
}

恭喜你完成了Flutter网络请求与API的学习! 现在你已经掌握了与服务器交互的核心技能!

posted on 2025-12-15 08:52  ljbguanli  阅读(26)  评论(0)    收藏  举报