第14讲:HTTP网络请求 - Dio库的使用与封装

导言:

在现代移动应用开发中,与服务器进行HTTP网络通信是必不可少的一环。Flutter提供了基础的 http 包,但功能相对简单。Dio 是一个强大且易用的Dart/Flutter HTTP客户端,它支持拦截器、全局配置、FormData、请求取消、文件上传/下载、超时等高级功能。本讲将带你从零开始,全面掌握Dio的使用,并学会如何对其进行企业级封装。


一、 为什么选择Dio?

在开始之前,我们简单对比一下原生 http 包和 dio

特性 Dart http Dio
易用性 基础,需要手动解析JSON API友好,支持多种数据转换
拦截器 不支持 强大支持,可全局处理请求/响应/错误
文件操作 需要自己处理流 原生支持文件上传/下载,进度回调
请求取消 不支持 支持
超时配置 全局配置 可针对每个请求配置
自动重试 需手动实现 通过拦截器可轻松实现

结论:对于大多数严肃的商业项目,Dio是不二之选


二、 基础使用:快速上手

1. 添加依赖

pubspec.yaml 文件中添加依赖:

yaml

复制

下载

复制代码
dependencies:
  dio: ^5.0.0 # 请使用最新版本

然后在终端运行 flutter pub get

2. 发起一个GET请求

dart

复制

下载

复制代码
import 'package:dio/dio.dart';

void fetchUserData() async {
  try {
    // 创建一个Dio实例
    final dio = Dio();
    
    // 发起GET请求
    final response = await dio.get('https://jsonplaceholder.typicode.com/users/1');
    
    // 请求成功,response.data 已经是解析好的Map或List
    print(response.data); // 直接是Map类型,无需手动jsonDecode
    print(response.statusCode); // HTTP状态码
    
  } on DioException catch (e) {
    // 使用 DioException 捕获 Dio 相关的错误
    // 这是 Dio 5.x 的变更,之前是 DioError
    print('请求出错: ${e.message}');
    if (e.response != null) {
      // 服务器返回了错误状态码(如404, 500等)
      print('服务器错误: ${e.response?.statusCode}');
      print('服务器错误信息: ${e.response?.data}');
    }
  } catch (e) {
    // 处理其他异常(如网络连接失败)
    print('其他错误: $e');
  }
}

3. 发起一个POST请求

dart

复制

下载

复制代码
void postUserData() async {
  final dio = Dio();
  
  try {
    final response = await dio.post(
      'https://jsonplaceholder.typicode.com/posts',
      data: {
        'title': 'foo',
        'body': 'bar',
        'userId': 1,
      }, // Dio会自动将Map编码为JSON
      options: Options(
        headers: {
          'Content-Type': 'application/json; charset=UTF-8',
        },
      ),
    );
    
    print('创建成功: ${response.data}');
  } on DioException catch (e) {
    print('提交失败: ${e.message}');
  }
}

三、 核心配置与全局初始化

在实际项目中,我们通常会创建一个全局的、配置好的Dio实例。

dart

复制

下载

复制代码
// network/dio_client.dart

class DioClient {
  // 静态实例
  static final DioClient _instance = DioClient._internal();
  
  // 工厂构造函数,返回单例
  factory DioClient() {
    return _instance;
  }
  
  // 内部的Dio实例
  final Dio dio;
  
  // 私有构造函数,在这里进行初始化
  DioClient._internal()
      : dio = Dio(
          BaseOptions(
            // 基础URL,后续所有请求会自动拼接
            baseUrl: 'https://jsonplaceholder.typicode.com',
            
            // 连接超时时间(毫秒)
            connectTimeout: const Duration(seconds: 10),
            
            // 接收超时时间(毫秒)
            receiveTimeout: const Duration(seconds: 10),
            
            // 发送超时时间(毫秒)
            sendTimeout: const Duration(seconds: 10),
            
            // 请求头
            headers: {
              'Content-Type': 'application/json',
            },
          ),
        ) {
    // 可以在这里添加拦截器
    _addInterceptors();
  }
  
  void _addInterceptors() {
    // 添加日志拦截器(需要 dio_logger 包)
    // dio.interceptors.add(LogInterceptor());
    
    // 添加自定义拦截器
    dio.interceptors.add(CustomInterceptor());
  }
}

四、 拦截器(Interceptors):Dio的灵魂

拦截器允许你在请求发出前或响应返回后,对其进行统一处理。

1. 自定义拦截器示例

dart

复制

下载

复制代码
// interceptors/custom_interceptor.dart

class CustomInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('🚀 发出请求: ${options.method} ${options.uri}');
    
    // 在实际项目中,可以在这里统一添加Token
    // options.headers['Authorization'] = 'Bearer $token';
    
    // 必须调用 handler.next 继续执行
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('✅ 收到响应: ${response.statusCode} ${response.requestOptions.uri}');
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    print('❌ 请求错误: ${err.type} - ${err.message}');
    
    // 统一错误处理
    _handleError(err);
    
    handler.next(err);
  }
  
  void _handleError(DioException err) {
    switch (err.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        // 显示超时错误提示
        break;
      case DioExceptionType.badResponse:
        // 处理服务器返回的错误状态码
        final statusCode = err.response?.statusCode;
        if (statusCode == 401) {
          // Token过期,跳转到登录页
          // _goToLogin();
        }
        break;
      case DioExceptionType.cancel:
        // 请求被取消
        break;
      case DioExceptionType.unknown:
        // 其他错误,通常是网络问题
        break;
      default:
        break;
    }
  }
}

2. 使用拦截器实现自动Token刷新

这是一个高级但非常实用的场景:

dart

复制

下载

复制代码
class TokenRefreshInterceptor extends Interceptor {
  final Dio _dio;
  final Future<String> Function() _refreshToken;
  
  TokenRefreshInterceptor(this._dio, this._refreshToken);

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // 如果是401错误且不是刷新Token的请求本身
    if (err.response?.statusCode == 401 && 
        !err.requestOptions.path.contains('/refresh-token')) {
      
      try {
        // 尝试刷新Token
        final newToken = await _refreshToken();
        
        // 更新请求头中的Token
        err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
        
        // 重新发起失败的请求
        final response = await _dio.fetch(err.requestOptions);
        handler.resolve(response); // 返回成功的结果
      } catch (e) {
        // 刷新Token失败,跳转到登录页
        // _goToLogin();
        handler.reject(err); // 继续抛出错误
      }
    } else {
      handler.next(err);
    }
  }
}

五、 企业级封装:ApiService模式

为了更好的维护性和可测试性,我们通常会对网络请求进行进一步封装。

1. 定义API端点

dart

复制

下载

复制代码
// api/endpoints.dart

abstract class ApiEndpoints {
  static const String baseUrl = 'https://jsonplaceholder.typicode.com';
  
  static const String getUser = '/users/{id}';
  static const String createPost = '/posts';
  static const String uploadImage = '/upload';
}

2. 创建ApiService

dart

复制

下载

复制代码
// services/api_service.dart

class ApiService {
  final Dio _dio;
  
  ApiService(this._dio);
  
  // 获取用户信息
  Future<User> getUser(int id) async {
    final response = await _dio.get(
      ApiEndpoints.getUser.replaceFirst('{id}', id.toString()),
    );
    return User.fromJson(response.data);
  }
  
  // 创建帖子
  Future<Post> createPost(Post post) async {
    final response = await _dio.post(
      ApiEndpoints.createPost,
      data: post.toJson(),
    );
    return Post.fromJson(response.data);
  }
  
  // 文件上传
  Future<String> uploadImage(String filePath) async {
    // 创建FormData
    final formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(filePath),
    });
    
    final response = await _dio.post(
      ApiEndpoints.uploadImage,
      data: formData,
      
      // 上传进度回调
      onSendProgress: (sent, total) {
        if (total != -1) {
          print('上传进度: ${(sent / total * 100).toStringAsFixed(0)}%');
        }
      },
    );
    
    return response.data['url']; // 假设返回图片URL
  }
}

3. 在项目中使用的完整示例

dart

复制

下载

复制代码
// main.dart 或依赖注入的设置

void main() {
  // 初始化Dio客户端
  final dioClient = DioClient();
  
  // 创建ApiService
  final apiService = ApiService(dioClient.dio);
  
  runApp(MyApp(apiService: apiService));
}

class MyApp extends StatelessWidget {
  final ApiService apiService;
  
  const MyApp({super.key, required this.apiService});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: UserProfilePage(apiService: apiService),
    );
  }
}

// 使用ApiService的页面
class UserProfilePage extends StatefulWidget {
  final ApiService apiService;
  
  const UserProfilePage({super.key, required this.apiService});
  
  @override
  State<UserProfilePage> createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  User? _user;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _fetchUserData();
  }

  Future<void> _fetchUserData() async {
    setState(() {
      _isLoading = true;
    });
    
    try {
      final user = await widget.apiService.getUser(1);
      setState(() {
        _user = user;
      });
    } on DioException catch (e) {
      // 显示错误提示
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('加载失败: ${e.message}')),
      );
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('用户信息')),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _user != null
              ? UserInfoWidget(user: _user!)
              : const Center(child: Text('加载失败')),
    );
  }
}

六、 错误处理的统一规范

为了在整个应用中保持一致的错误处理体验,建议创建统一的错误处理工具:

dart

复制

下载

复制代码
// utils/error_handler.dart

class ErrorHandler {
  static String getErrorMessage(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return '网络连接超时,请检查网络后重试';
      case DioExceptionType.badResponse:
        switch (e.response?.statusCode) {
          case 401:
            return '登录已过期,请重新登录';
          case 404:
            return '请求的资源不存在';
          case 500:
          case 502:
          case 503:
            return '服务器开小差了,请稍后重试';
          default:
            return '网络请求失败: ${e.response?.statusCode}';
        }
      case DioExceptionType.cancel:
        return '请求已取消';
      case DioExceptionType.unknown:
        return '网络连接失败,请检查网络设置';
      default:
        return '未知错误,请稍后重试';
    }
  }
}

七、 总结与最佳实践

通过本讲的学习,你应该已经掌握了:

  1. Dio的基础使用:GET/POST请求,错误处理。

  2. 全局配置:创建配置良好的单例Dio客户端。

  3. 拦截器:日志记录、统一错误处理、Token自动刷新。

  4. 企业级封装:ApiService模式,职责分离。

  5. 文件上传:使用FormData进行文件操作。

相关推荐
报错小能手2 小时前
计算机网络自顶向下方法33——网络层 路由器工作原理 输入端口处理和基于目的地转发 交换 输出端口处理
网络·计算机网络·智能路由器
scd02082 小时前
11.10dns作业
运维·服务器·网络
L.EscaRC3 小时前
【复习408】TCP运输层核心机制
网络协议·tcp/ip
红树林073 小时前
渗透测试之json_web_token(JWT)
网络协议·安全·web安全
2501_915918413 小时前
HTTP和HTTPS工作原理、安全漏洞及防护措施全面解析
android·http·ios·小程序·https·uni-app·iphone
Yurko133 小时前
【计网】基于三层交换机和 RIP 协议的局域网组建
网络·学习·计算机网络·智能路由器
无序的浪4 小时前
网络初识~
网络
wzlsunice884 小时前
用vir-manager创建kvm虚拟机(创建网桥和配置网络等)
运维·网络
北京耐用通信4 小时前
冶金车间“迷雾”重重?耐达讯自动化Profibus转光纤为HMI点亮“透视眼”!
人工智能·物联网·网络协议·网络安全·自动化