MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter dio插件的拦截器:实现请求与响应的统一处理

2024-01-096.1k 阅读

1. Flutter 与 Dio 插件简介

Flutter 是谷歌开发的一款开源 UI 框架,用于构建跨平台的原生应用。它采用 Dart 语言进行开发,能够让开发者使用一套代码库同时生成 iOS 和 Android 应用,大大提高了开发效率。

Dio 是 Flutter 生态中一款强大的 HTTP 请求库,它支持诸如拦截器、全局配置、FormData 上传等丰富功能。Dio 以其简洁易用且高效的特点,在 Flutter 开发中被广泛应用于处理网络请求,成为众多开发者的首选。

2. 为什么需要拦截器

在实际的应用开发中,我们常常需要对网络请求和响应进行一些统一的处理。例如,在每次请求前添加通用的请求头,如身份验证令牌(Token);在请求失败时进行统一的错误处理,或者在响应回来后对数据进行统一的解析等。如果在每个发起请求的地方都去手动添加这些逻辑,不仅代码冗余,而且后期维护成本高。

拦截器的出现很好地解决了这个问题。通过拦截器,我们可以在请求发出前和响应返回后,统一地执行特定的逻辑,实现代码的复用和优化,使网络请求部分的代码结构更加清晰。

3. Dio 拦截器基础

Dio 中的拦截器分为请求拦截器和响应拦截器。请求拦截器会在请求被发送之前被调用,我们可以在这个阶段对请求进行一些预处理,比如添加请求头、修改请求参数等。响应拦截器则是在收到服务器响应之后被调用,我们可以在这里对响应进行处理,例如数据解析、错误处理等。

3.1 添加拦截器

在 Dio 中添加拦截器非常简单。首先,我们需要引入 Dio 库:

import 'package:dio/dio.dart';

然后创建一个 Dio 实例,并添加拦截器:

void main() {
  Dio dio = Dio();
  // 添加请求拦截器
  dio.interceptors.add(InterceptorsWrapper(
    onRequest: (RequestOptions options) {
      print('请求被拦截,即将发送请求:$options');
      return options;
    },
  ));
  // 添加响应拦截器
  dio.interceptors.add(InterceptorsWrapper(
    onResponse: (Response response) {
      print('响应被拦截,收到响应:$response');
      return response;
    },
  ));
}

在上述代码中,InterceptorsWrapper 是 Dio 提供的一个拦截器包装类,我们通过它来定义具体的拦截逻辑。onRequest 回调函数用于处理请求拦截,onResponse 回调函数用于处理响应拦截。

3.2 请求拦截器的参数

onRequest 回调函数接收一个 RequestOptions 参数,RequestOptions 包含了请求的所有信息,例如请求的 URL、方法(GET、POST 等)、请求头、请求体等。我们可以通过修改 RequestOptions 的属性来修改请求的内容。

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (RequestOptions options) {
    // 添加通用请求头
    options.headers['Authorization'] = 'Bearer your_token';
    return options;
  },
));

在这个例子中,我们在每个请求的请求头中添加了一个 Authorization 字段,值为 Bearer your_token,这在需要身份验证的接口请求中非常常见。

3.3 响应拦截器的参数

onResponse 回调函数接收一个 Response 参数,Response 包含了服务器返回的响应信息,如状态码、响应头、响应体等。我们可以在这里对响应数据进行处理。

dio.interceptors.add(InterceptorsWrapper(
  onResponse: (Response response) {
    if (response.statusCode == 200) {
      print('请求成功,响应数据:${response.data}');
    } else {
      print('请求失败,状态码:${response.statusCode}');
    }
    return response;
  },
));

在这个例子中,我们根据响应的状态码来判断请求是否成功,并打印相应的信息。

4. 实现请求与响应的统一处理

4.1 统一添加请求头

在许多应用中,我们需要在每个请求中添加一些通用的请求头,如 Content - Type、身份验证信息等。通过请求拦截器,我们可以很方便地实现这一点。

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (RequestOptions options) {
    options.headers['Content - Type'] = 'application/json';
    // 假设从本地存储获取 Token
    String? token = getTokenFromLocalStorage();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    return options;
  },
));

上述代码中,我们首先设置了 Content - Typeapplication/json,表示请求体是 JSON 格式的数据。然后从本地存储获取用户的 Token,并添加到 Authorization 请求头中。这样,每个请求都会带上这些通用的请求头。

4.2 统一错误处理

在响应拦截器中,我们可以对请求失败的情况进行统一处理。常见的错误包括网络错误、服务器返回的错误状态码等。

dio.interceptors.add(InterceptorsWrapper(
  onResponse: (Response response) {
    if (response.statusCode! >= 400) {
      // 处理 400 及以上的状态码错误
      print('请求失败,状态码:${response.statusCode}');
      showErrorDialog('请求失败,请稍后重试');
      return Future.error( DioError(requestOptions: response.requestOptions, error: '请求失败,状态码:${response.statusCode}'));
    }
    return response;
  },
  onError: (DioError e) {
    // 处理网络错误等其他错误
    print('发生错误:${e.message}');
    showErrorDialog('网络连接失败,请检查网络');
    return Future.error(e);
  },
));

在这段代码中,我们在 onResponse 回调中处理服务器返回的 400 及以上状态码的错误,弹出一个错误提示对话框,并返回一个错误的 Future。在 onError 回调中处理网络连接等其他错误,同样弹出错误提示对话框。这样,所有的错误都能得到统一的处理,提升了用户体验。

4.3 统一数据解析

通常,服务器返回的数据需要进行一定的解析才能在应用中使用。我们可以在响应拦截器中统一进行数据解析。假设服务器返回的数据格式如下:

{
  "code": 200,
  "message": "成功",
  "data": {
    "name": "John",
    "age": 30
  }
}

我们可以在响应拦截器中这样解析数据:

dio.interceptors.add(InterceptorsWrapper(
  onResponse: (Response response) {
    if (response.statusCode == 200) {
      Map<String, dynamic> data = response.data;
      if (data['code'] == 200) {
        return data['data'];
      } else {
        print('业务逻辑错误,错误信息:${data['message']}');
        showErrorDialog(data['message']);
        return Future.error( DioError(requestOptions: response.requestOptions, error: '业务逻辑错误,错误信息:${data['message']}'));
      }
    }
    return response;
  },
));

在这个例子中,我们首先判断状态码是否为 200,如果是,则进一步判断业务逻辑返回的 code 是否为 200。如果业务 code 为 200,就返回实际需要的数据部分(data 字段);否则,处理业务逻辑错误,弹出错误提示对话框并返回错误的 Future

5. 自定义拦截器类

虽然使用 InterceptorsWrapper 可以快速定义拦截器逻辑,但在实际项目中,为了代码的可维护性和复用性,我们通常会创建自定义的拦截器类。

class MyInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers['Content - Type'] = 'application/json';
    String? token = getTokenFromLocalStorage();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (response.statusCode! >= 400) {
      print('请求失败,状态码:${response.statusCode}');
      showErrorDialog('请求失败,请稍后重试');
      handler.reject( DioError(requestOptions: response.requestOptions, error: '请求失败,状态码:${response.statusCode}'));
    }
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print('发生错误:${err.message}');
    showErrorDialog('网络连接失败,请检查网络');
    handler.reject(err);
  }
}

在上述代码中,我们创建了一个 MyInterceptor 类,它继承自 Interceptor 类。然后重写了 onRequestonResponseonError 方法来实现我们自定义的拦截逻辑。在使用时,我们可以这样添加拦截器:

Dio dio = Dio();
dio.interceptors.add(MyInterceptor());

这样,我们就可以将拦截器逻辑封装在一个类中,方便在不同的地方复用,并且使代码结构更加清晰。

6. 多个拦截器的执行顺序

当我们添加多个拦截器时,了解它们的执行顺序是很重要的。请求拦截器按照添加的顺序依次执行,而响应拦截器则按照添加顺序的相反顺序执行。

例如,我们添加了三个拦截器 Interceptor1Interceptor2Interceptor3

Dio dio = Dio();
dio.interceptors.add(Interceptor1());
dio.interceptors.add(Interceptor2());
dio.interceptors.add(Interceptor3());

在请求阶段,执行顺序为 Interceptor1.onRequest -> Interceptor2.onRequest -> Interceptor3.onRequest。在响应阶段,执行顺序为 Interceptor3.onResponse -> Interceptor2.onResponse -> Interceptor1.onResponse。如果某个拦截器在 onRequest 中返回了一个 Future.error,那么后续的请求拦截器将不会执行,并且会直接进入 onError 回调。同样,如果在 onResponse 中返回了一个 Future.error,后续的响应拦截器也不会执行。

7. 拦截器在复杂业务场景中的应用

7.1 重试机制

在网络不稳定的情况下,请求可能会偶尔失败。我们可以通过拦截器实现一个简单的重试机制。

class RetryInterceptor extends Interceptor {
  int maxRetryCount = 3;

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.type == DioErrorType.connectTimeout || err.type == DioErrorType.receiveTimeout) {
      int retryCount = 0;
      while (retryCount < maxRetryCount) {
        try {
          Response response = await err.requestOptions.retry();
          handler.resolve(response);
          return;
        } catch (e) {
          retryCount++;
        }
      }
      handler.reject(err);
    } else {
      handler.reject(err);
    }
  }
}

在上述代码中,RetryInterceptor 拦截器在捕获到连接超时或接收超时的错误时,会尝试重新发起请求,最多重试 maxRetryCount 次。如果重试成功,就通过 handler.resolve 返回响应;如果重试次数用尽仍失败,则通过 handler.reject 返回错误。

7.2 日志记录

在开发和调试过程中,记录网络请求和响应的详细信息是非常有帮助的。我们可以通过拦截器来实现日志记录功能。

class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('请求 URL:${options.uri}');
    print('请求方法:${options.method}');
    print('请求头:${options.headers}');
    if (options.data != null) {
      print('请求体:${options.data}');
    }
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('响应状态码:${response.statusCode}');
    print('响应头:${response.headers}');
    print('响应体:${response.data}');
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print('错误信息:${err.message}');
    handler.reject(err);
  }
}

LoggingInterceptor 拦截器在请求、响应和错误发生时,分别打印出相关的详细信息,帮助开发者快速定位问题。

8. 注意事项

  1. 内存泄漏:在拦截器中,如果使用了一些需要手动释放资源的对象,如流(Stream)、定时器(Timer)等,一定要注意在适当的时候释放资源,避免内存泄漏。
  2. 性能问题:虽然拦截器可以方便地统一处理请求和响应,但过多的拦截器或者复杂的拦截逻辑可能会影响性能。尽量保持拦截器逻辑的简洁,避免在拦截器中执行耗时操作。
  3. 版本兼容性:Dio 库可能会随着时间推移进行更新,新的版本可能会对拦截器的 API 有一些改动。在升级 Dio 版本时,要注意检查拦截器代码是否仍然兼容。

通过合理使用 Dio 的拦截器,我们可以有效地实现请求与响应的统一处理,提高代码的复用性和可维护性,打造出更加健壮和高效的 Flutter 应用。无论是简单的通用请求头添加,还是复杂的重试机制和日志记录,拦截器都能为我们提供强大的支持。在实际开发中,根据具体的业务需求,灵活运用拦截器,将会极大地提升开发效率和应用质量。