在flutter中dio应该如何封装和使用

在 Flutter 中,Dio 是最常用的网络请求库,合理的封装可以提升代码复用性、降低维护成本,还能统一处理请求拦截、错误处理、超时配置等通用逻辑。以下是一套标准化的 Dio 封装方案,包含核心封装、拦截器配置、业务适配和使用示例。

一、核心封装思路

  1. 单例模式:避免重复创建 Dio 实例,统一管理配置;
  2. 拦截器:统一处理请求头、请求 / 响应日志、token 刷新、错误处理;
  3. 通用方法封装:封装 GET/POST/PUT/DELETE 等基础请求,简化业务层调用;
  4. 数据解析:统一处理接口返回格式,剥离业务数据与通用包装;
  5. 异常处理:封装自定义异常,区分网络错误、业务错误、超时错误等。

二、完整封装实现

步骤 1:添加依赖

pubspec.yaml 中添加 Dio 依赖(建议指定稳定版本):

yaml

yaml 复制代码
dependencies:
  dio: ^5.4.3+1  # 最新版本可查看 pub.dev
  flutter_dotenv: ^5.1.0  # 可选,用于管理环境变量(如 baseUrl)
  cookie_jar: ^4.0.1      # 可选,用于 cookie 管理

步骤 2:创建网络配置文件

新建 lib/core/net/network_config.dart,管理基础配置:

dart

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

/// 网络请求配置
class NetworkConfig {
  /// 基础域名(可通过环境变量区分开发/生产环境)
  static String baseUrl = dotenv.env['BASE_URL'] ?? 'https://api.example.com';

  /// 请求超时时间(毫秒)
  static const int connectTimeout = 10000;

  /// 响应超时时间(毫秒)
  static const int receiveTimeout = 10000;

  /// 默认请求头
  static Map<String, String> defaultHeaders = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  };
}

步骤 3:封装 Dio 实例(核心)

新建 lib/core/net/dio_client.dart,实现 Dio 单例和拦截器:

dart

dart 复制代码
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'network_config.dart';
import 'network_exception.dart'; // 后续创建自定义异常

/// Dio 单例客户端
class DioClient {
  static DioClient? _instance;
  late Dio _dio;

  /// 私有构造方法
  DioClient._internal() {
    // 初始化 Dio
    _dio = Dio(
      BaseOptions(
        baseUrl: NetworkConfig.baseUrl,
        connectTimeout: Duration(milliseconds: NetworkConfig.connectTimeout),
        receiveTimeout: Duration(milliseconds: NetworkConfig.receiveTimeout),
        headers: NetworkConfig.defaultHeaders,
        responseType: ResponseType.json,
      ),
    );

    // 添加拦截器
    _addInterceptors();
  }

  /// 获取单例
  static DioClient get instance {
    _instance ??= DioClient._internal();
    return _instance!;
  }

  /// 获取原始 Dio 实例(特殊场景使用)
  Dio get dio => _dio;

  /// 添加拦截器
  void _addInterceptors() {
    // 1. Cookie 拦截器(可选,根据业务需求)
    _dio.interceptors.add(CookieManager(CookieJar()));

    // 2. 日志拦截器(开发环境开启,生产环境关闭)
    _dio.interceptors.add(
      LogInterceptor(
        request: true, // 打印请求信息
        requestHeader: true, // 打印请求头
        requestBody: true, // 打印请求体
        responseHeader: true, // 打印响应头
        responseBody: true, // 打印响应体
        error: true, // 打印错误信息
      ),
    );

    // 3. 自定义拦截器(处理 token、请求头、响应统一解析等)
    _dio.interceptors.add(
      InterceptorsWrapper(
        // 请求拦截
        onRequest: (RequestOptions options, RequestInterceptorHandler handler) async {
          // 示例:添加 token 到请求头
          String? token = _getToken(); // 从本地缓存获取 token
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          handler.next(options); // 继续请求
        },

        // 响应拦截
        onResponse: (Response response, ResponseInterceptorHandler handler) {
          // 统一解析响应数据(假设接口返回格式:{code: 200, msg: "成功", data: {...}})
          Map<String, dynamic> data = response.data;
          int code = data['code'] ?? -1;
          String msg = data['msg'] ?? '请求失败';

          if (code == 200) {
            // 业务成功,只返回 data 给上层
            response.data = data['data'];
            handler.next(response);
          } else {
            // 业务错误,抛出自定义异常
            handler.reject(
              DioException(
                requestOptions: response.requestOptions,
                error: NetworkException(code: code, msg: msg),
              ),
            );
          }
        },

        // 错误拦截
        onError: (DioException e, ErrorInterceptorHandler handler) {
          // 统一处理错误(网络错误、超时、业务错误等)
          NetworkException exception = _convertDioErrorToNetworkException(e);
          handler.reject(
            DioException(
              requestOptions: e.requestOptions,
              error: exception,
              type: e.type,
              message: exception.msg,
            ),
          );
        },
      ),
    );
  }

  /// 从本地缓存获取 token(示例方法,需根据实际实现)
  String? _getToken() {
    // 示例:从 SharedPreferences 获取
    // SharedPreferences prefs = await SharedPreferences.getInstance();
    // return prefs.getString('token');
    return null;
  }

  /// 将 Dio 错误转换为自定义网络异常
  NetworkException _convertDioErrorToNetworkException(DioException e) {
    if (e.type == DioExceptionType.connectionTimeout ||
        e.type == DioExceptionType.receiveTimeout ||
        e.type == DioExceptionType.sendTimeout) {
      return NetworkException(code: -1, msg: '请求超时,请检查网络');
    } else if (e.type == DioExceptionType.connectionError) {
      return NetworkException(code: -2, msg: '网络连接失败,请检查网络');
    } else if (e.type == DioExceptionType.badResponse) {
      // HTTP 状态码错误(404、500 等)
      int statusCode = e.response?.statusCode ?? -3;
      return NetworkException(code: statusCode, msg: '服务器错误($statusCode)');
    } else if (e.error is NetworkException) {
      // 业务错误(已在响应拦截器抛出)
      return e.error as NetworkException;
    } else {
      return NetworkException(code: -999, msg: '未知错误:${e.message}');
    }
  }

  // ---------------------- 通用请求方法封装 ----------------------
  /// GET 请求
  Future<T> get<T>(
    String path, {
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      Response response = await _dio.get(
        path,
        queryParameters: params,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw e.error as NetworkException;
    }
  }

  /// POST 请求
  Future<T> post<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      Response response = await _dio.post(
        path,
        data: data,
        queryParameters: params,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw e.error as NetworkException;
    }
  }

  /// PUT 请求
  Future<T> put<T>(
    String path, {
    dynamic data,
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      Response response = await _dio.put(
        path,
        data: data,
        queryParameters: params,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw e.error as NetworkException;
    }
  }

  /// DELETE 请求
  Future<T> delete<T>(
    String path, {
    Map<String, dynamic>? params,
    Options? options,
    CancelToken? cancelToken,
  }) async {
    try {
      Response response = await _dio.delete(
        path,
        queryParameters: params,
        options: options,
        cancelToken: cancelToken,
      );
      return response.data as T;
    } on DioException catch (e) {
      throw e.error as NetworkException;
    }
  }

  /// 上传文件
  Future<T> upload<T>(
    String path, {
    required MultipartFile file,
    String fileName = 'file',
    Map<String, dynamic>? params,
    CancelToken? cancelToken,
    ProgressCallback? onSendProgress,
  }) async {
    try {
      FormData formData = FormData.fromMap({
        fileName: file,
        if (params != null) ...params,
      });
      Response response = await _dio.post(
        path,
        data: formData,
        onSendProgress: onSendProgress,
        cancelToken: cancelToken,
        options: Options(
          contentType: 'multipart/form-data',
        ),
      );
      return response.data as T;
    } on DioException catch (e) {
      throw e.error as NetworkException;
    }
  }
}

步骤 4:封装自定义网络异常

新建 lib/core/net/network_exception.dart,统一异常类型:

dart 复制代码
/// 自定义网络异常(区分业务错误和系统错误)
class NetworkException implements Exception {
  final int code; // 错误码(业务码/HTTP状态码/系统码)
  final String msg; // 错误信息

  NetworkException({required this.code, required this.msg});

  @override
  String toString() => 'NetworkException(code: $code, msg: $msg)';
}

步骤 5:业务层 API 封装(示例)

新建 lib/api/user_api.dart,封装具体业务接口:

dart

dart 复制代码
import 'package:your_project/core/net/dio_client.dart';
import 'package:your_project/core/net/network_exception.dart';

/// 用户相关 API
class UserApi {
  static final DioClient _dioClient = DioClient.instance;

  /// 登录接口
  static Future<Map<String, dynamic>> login({
    required String username,
    required String password,
  }) async {
    try {
      return await _dioClient.post(
        '/user/login',
        data: {
          'username': username,
          'password': password,
        },
      );
    } on NetworkException catch (e) {
      // 可在此处针对特定错误码做处理(如登录失败、账号冻结等)
      if (e.code == 401) {
        throw NetworkException(code: 401, msg: '用户名或密码错误');
      }
      rethrow; // 其他错误抛给上层处理
    }
  }

  /// 获取用户信息
  static Future<Map<String, dynamic>> getUserInfo() async {
    return await _dioClient.get('/user/info');
  }
}

三、在页面中使用

dart

php 复制代码
import 'package:flutter/material.dart';
import 'package:your_project/api/user_api.dart';
import 'package:your_project/core/net/network_exception.dart';

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  /// 登录按钮点击事件
  Future<void> _login() async {
    String username = _usernameController.text.trim();
    String password = _passwordController.text.trim();

    if (username.isEmpty || password.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请输入用户名和密码')),
      );
      return;
    }

    try {
      // 显示加载中
      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (context) => const Center(child: CircularProgressIndicator()),
      );

      // 调用登录接口
      Map<String, dynamic> userData = await UserApi.login(
        username: username,
        password: password,
      );

      // 登录成功,处理逻辑(如保存 token、跳转到首页)
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('登录成功')),
      );
      Navigator.pop(context); // 关闭加载弹窗
      // Navigator.pushReplacementNamed(context, '/home');

    } on NetworkException catch (e) {
      // 处理错误
      Navigator.pop(context); // 关闭加载弹窗
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.msg)),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(hintText: '用户名'),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(hintText: '密码'),
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: _login,
              child: const Text('登录'),
            ),
          ],
        ),
      ),
    );
  }
}

四、进阶优化点

  1. 环境切换 :通过 flutter_dotenv 管理开发 / 测试 / 生产环境的 baseUrl
  2. Token 刷新:在请求拦截器中检测 token 过期,自动刷新 token 并重试请求;
  3. 取消请求 :使用 CancelToken 处理页面销毁时取消未完成的请求,避免内存泄漏;
  4. 缓存策略 :结合 dio_cache_interceptor 实现 GET 请求缓存;
  5. 重试机制:添加重试拦截器,针对网络波动自动重试请求;
  6. SSL 证书校验:对接后台 HTTPS 证书,防止抓包(生产环境必备);
  7. 请求加密:对敏感请求(如登录)进行参数加密 / 签名。

五、核心优势

  1. 统一管理:所有网络配置、拦截器集中管理,修改方便;
  2. 简化调用:业务层只需关注接口参数和返回值,无需处理通用逻辑;
  3. 错误统一 :所有错误转换为自定义异常,上层只需捕获 NetworkException
  4. 可扩展性:新增拦截器、修改配置不影响业务代码。

这套封装方案适配大多数 Flutter 项目的网络需求,可根据实际业务调整(如接口返回格式、token 逻辑、异常类型等)。

相关推荐
汉堡大王95271 小时前
JavaScript类型侦探:四大神器让你一眼看穿变量真身
前端·javascript
Debroon1 小时前
从零开始手写ReAct Agent
前端·javascript·react.js
Hello.Reader1 小时前
Rocket 0.5 快速上手3 分钟跑起第一个 Rust Web 服务
开发语言·前端·rust
YIN_O1 小时前
openEuler 上 CUDA 与 ROCm 的 GPU 加速实战
前端
古城小栈1 小时前
前端安全进阶:有效防止页面被调试、数据泄露
前端·安全·状态模式
chilavert3181 小时前
技术演进中的开发沉思-230 Ajax:Prototype.js 重构原生 DOM
开发语言·前端·javascript
手握风云-1 小时前
JavaEE 进阶第七期:Spring MVC - Web开发的“交通枢纽”(一)
前端·spring·java-ee
Rysxt_1 小时前
Vue 集成富文本编辑器教程
前端·javascript·vue.js·富文本
开发者小天1 小时前
React中的受控组件示例
前端·javascript·react.js