前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter之网络请求封装

Flutter之网络请求封装

作者头像
loongwind
发布2022-09-27 11:18:07
7.4K1
发布2022-09-27 11:18:07
举报
文章被收录于专栏:loongwind

应用开发中,网络请求几乎是必不可少的功能,本文将介绍如何通过对 dio 进行二次封装一步一步实现网络请求封装,以便于在项目中方便快捷的使用网络请求。

封装后的网络请求将具备如下功能:

•简单易用•数据解析•异常处理•请求拦截•日志打印• loading 显示

下面将一步一步带你实现网络请求的封装。

添加依赖

首先在项目里添加 dio 的依赖:

代码语言:javascript
复制
dependencies:
  dio: ^4.0.4

请求封装

首先创建一个 RequestConfig 类,用于放置 dio 的配置参数,如下:

代码语言:javascript
复制
class RequestConfig{
  static const baseUrl = "https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test";
  static const connectTimeout = 15000;
  static const successCode = 200;
}

配置了请求的 baseUrl 、连接超时时间、请求成功的业务编码。如果还有需其他配置也可以统一配置到该类下。

创建 RequestClient 用于封装 dio 的请求,在类的构造方法中初始化 dio 配置:

代码语言:javascript
复制
RequestClient requestClient = RequestClient();

class RequestClient {
  late Dio _dio;

  RequestClient() {
    _dio = Dio(
        BaseOptions(baseUrl: RequestConfig.baseUrl, connectTimeout: RequestConfig.connectTimeout)
    );
  }
}

在类的上方,创建了一个全局的变量 requestClient 方便外部调用。

dio 本身提供了getpostputdelete 等一系列 http 请求方法,但是通过源码发现最终这些方法都是调用的 request 的方法实现的。所以这里直接对 dio 的 request 方法进行封装。

代码语言:javascript
复制
Future<dynamic> request(
    String url, {
    String method = "GET",
    Map<String, dynamic>? queryParameters,
    data,
    Map<String, dynamic>? headers
  }) async {
    Options options = Options()
      ..method = method
      ..headers = headers;

    Response response = await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return response.data;
  }

将常用参数进行统一封装为 request 方法然后调用 dio 的 request 方法,然后再在 request 方法里进行统一的数据处理,如数据解析等。

数据解析

返回数据解析

在移动开发中,开发者习惯将返回数据解析成实体类使用,接下来将介绍如何结合 dio 完成数据解析的封装。

项目开发中接口返回的数据结构一般是这样的:

代码语言:javascript
复制
{
  "code": 200,
  "message": "success",
  "data":{
    "id": "12312312",
    "name": "loongwind",
    "age": 18
  }
}

创建 ApiResponse 类用于解析接口返回数据:

代码语言:javascript
复制
class ApiResponse<T> {

    int? code;
    String? message;
    T? data;

  ApiResponse();

  factory ApiResponse.fromJson(Map<String, dynamic> json) => $ApiResponseFromJson<T>(json);

  Map<String, dynamic> toJson() => $ApiResponseToJson(this);

  @override
  String toString() {
    return jsonEncode(this);
  }
}

因为返回的数据中 data 的数据类型是不定的,所以改造 request 支持泛型,然后在 request 方法中统一进行数据解析,然后返回 data 数据,代码如下:

代码语言:javascript
复制
Future<T?> request<T>(
    String url, {
    String method = "GET",
    Map<String, dynamic>? queryParameters,
    data,
    Map<String, dynamic>? headers
  }) async {
    Options options = Options()
      ..method = method
      ..headers = headers;

    Response response = await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return _handleRequestResponse<T>(response);
  }

  ///请求响应内容处理
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
      return _handleBusinessResponse<T>(apiResponse);
    } else {
      return null;
    }
  }

  ///业务内容处理
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      return null;
    }
  }

通过 ApiResponse 解析返回数据,然后判断 ApiResponse 的业务 code 是否为成功,成功则返回 data 数据。

有时候在应用里还需要调用第三方接口,但是第三方接口返回的数据结构可能会有差异,此时就需要返回原始数据单独做处理。创建一个 RawData 类,用于解析原始数据:

代码语言:javascript
复制
class RawData{
  dynamic value;
}

然后修改 RequestClient 中的 _handleResponse

代码语言:javascript
复制
///请求响应内容处理
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      if(T.toString() == (RawData).toString()){
        RawData raw = RawData();
        raw.value = response.data;
        return raw as T;
      }else {
        ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
        return _handleBusinessResponse<T>(apiResponse);
      }
    } else {
      var exception = ApiException(response.statusCode, ApiException.unknownException);
      throw exception;
    }
  }

新增判断泛型是否为 RawData ,是则直接去除 response.data 放入 RawData 中返回,即 RawData 的 value 就是接口返回的原始数据。

请求数据转换

除了返回数据的解析,实际开发过程中还会遇到对请求参数的处理,比如请求参数为 json 数据,但是代码里为了方便处理使用的实体类,request 中 data 参数可能传入的是一个实体类实例,此时就需要将 data 转换为 json 数据再进行数据请求。

代码语言:javascript
复制
 _convertRequestData(data) {
    if (data != null) {
      data = jsonDecode(jsonEncode(data));
    }
    return data;
  }

  Future<T?> request<T>(
    String url, {
    String method = "GET",
    Map<String, dynamic>? queryParameters,
    data,
    Map<String, dynamic>? headers
  }) async {
        ///...
    data = _convertRequestData(data);

    Response response = await _dio.request(url,
        queryParameters: queryParameters, data: data, options: options);

    return _handleResponse<T>(response);
  }
}

此处使用 _convertRequestData 方法,将请求 data 数据先使用 jsonEncode 转换为字符串,再使用 jsonDecode 方法将字符串转换为 Map。

异常处理

接下来看看如何进行统一的异常处理,异常一般分为两部分:Http异常、业务异常。

•Http 异常:Http 错误,如 404、503 等•业务异常:请求成功,但是业务异常,如:登录时用户名密码错误等

首先创建一个 ApiException 用于统一封装请求的异常信息:

代码语言:javascript
复制
class ApiException implements Exception {
  static const unknownException = "未知错误";
  final String? message;
  final int? code;
  String? stackInfo;

  ApiException([this.code, this.message]);

  factory ApiException.fromDioError(DioError error) {
    switch (error.type) {
      case DioErrorType.cancel:
        return BadRequestException(-1, "请求取消");
      case DioErrorType.connectTimeout:
        return BadRequestException(-1, "连接超时");
      case DioErrorType.sendTimeout:
        return BadRequestException(-1, "请求超时");
      case DioErrorType.receiveTimeout:
        return BadRequestException(-1, "响应超时");
      case DioErrorType.response:
        try {

          /// http 错误码带业务错误信息
          ApiResponse apiResponse = ApiResponse.fromJson(error.response?.data);
          if(apiResponse.code != null){
            return ApiException(apiResponse.code, apiResponse.message);
          }

          int? errCode = error.response?.statusCode;
          switch (errCode) {
            case 400:
              return BadRequestException(errCode, "请求语法错误");
            case 401:
              return UnauthorisedException(errCode!, "没有权限");
            case 403:
              return UnauthorisedException(errCode!, "服务器拒绝执行");
            case 404:
              return UnauthorisedException(errCode!, "无法连接服务器");
            case 405:
              return UnauthorisedException(errCode!, "请求方法被禁止");
            case 500:
              return UnauthorisedException(errCode!, "服务器内部错误");
            case 502:
              return UnauthorisedException(errCode!, "无效的请求");
            case 503:
              return UnauthorisedException(errCode!, "服务器异常");
            case 505:
              return UnauthorisedException(errCode!, "不支持HTTP协议请求");
            default:
              return ApiException(
                  errCode, error.response?.statusMessage ?? '未知错误');
          }
        } on Exception catch (e) {
          return ApiException(-1, unknownException);
        }
      default:
        return ApiException(-1, error.message);
    }
  }

  factory ApiException.from(dynamic exception){
    if(exception is DioError){
      return ApiException.fromDioError(exception);
    } if(exception is ApiException){
      return exception;
    } else {
      var apiException = ApiException(-1, unknownException);
      apiException.stackInfo = exception?.toString();
      return apiException;
    }
  }
}

/// 请求错误
class BadRequestException extends ApiException {
  BadRequestException([int? code, String? message]) : super(code, message);
}

/// 未认证异常
class UnauthorisedException extends ApiException {
  UnauthorisedException([int code = -1, String message = ''])
      : super(code, message);
}

ApiException 主要根据 DioError 信息创建 ApiException,但是仔细发现其中有一段解析返回数据让创建 ApiException 的代码,如下:

代码语言:javascript
复制
ApiResponse apiResponse = ApiResponse.fromJson(error.response?.data);
if(apiResponse.code != null){
  return ApiException(apiResponse.code, apiResponse.message);
}

是因为有些时候后端业务异常时修改了返回的 http 状态码,当 http 状态码非 200 开头时 dio 会抛出 DioError 错误,但此时需要的错误信息为 response 中的错误信息,所以这里需要先解析 response 数据获取错误信息。

ApiException 类创建好后,需要在 request 方法中捕获异常,对 request 方法改造如下:

代码语言:javascript
复制
Future<T?> request<T>(
    String url, {
    String method = "Get",
    Map<String, dynamic>? queryParameters,
    data,
    Map<String, dynamic>? headers,
    bool Function(ApiException)? onError,
  }) async {
    try {
      Options options = Options()
        ..method = method
        ..headers = headers;

      data = _convertRequestData(data);

      Response response = await _dio.request(url,
          queryParameters: queryParameters, data: data, options: options);

      return _handleResponse<T>(response);
    } catch (e) {
      var exception = ApiException.from(e);
      if(onError?.call(exception) != true){
        throw exception;
      }
    }

    return null;
  }

  ///请求响应内容处理
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      ApiResponse<T> apiResponse = ApiResponse<T>.fromJson(response.data);
      return _handleBusinessResponse<T>(apiResponse);
    } else {
      var exception = ApiException(response.statusCode, ApiException.unknownException);
      throw exception;
    }
  }

  ///业务内容处理
  T? _handleBusinessResponse<T>(ApiResponse<T> response) {
    if (response.code == RequestConfig.successCode) {
      return response.data;
    } else {
      var exception = ApiException(response.code, response.message);
      throw exception;
    }
  }

在 request 方法上添加了 bool Function(ApiException)? onError 参数,用于错误信息处理的回调,且返回值为 bool

request 方法中添加 try-catch 包裹,并在 catch 中创建 ApiException ,调用 onError,当 onError 返回为 true 时即错误信息已被调用方处理,则不抛出异常,否则抛出异常。

同时为 response 数据解析的方法也加上了抛出异常的处理。当业务异常时抛出对应的业务异常信息。

经过上述封装后,确实能对异常信息进行处理,但在实际开发中有个问题,开发中经常会在接口请求成功后做其他处理,比如数据处理或者界面刷新等,请求失败后弹出提示或者错误处理等等,如果按照上述的封装则需要判断返回数据是否为 null 不为空进行后续处理,如果一个业务存在多个请求依赖调用,则此处则会嵌套多次,代码阅读性不好。如下:

代码语言:javascript
复制
var data1 = requestClient.request(url1);
if( data1 != null ){
  var data2 = requestClient.request(url2);
  if(data2 != null){
    var data3 = requestClient.request(url3);
    ///...
  }
}

为了解决上述问题,并且实现统一异常处理,创建一个顶级的 request 方法:

代码语言:javascript
复制
Future request(Function() block,  {bool Function(ApiException)? onError}) async{
  try {
    await block();
  } catch (e) {
    handleException(ApiException.from(e), onError: onError);
  }
  return;
}


bool handleException(ApiException exception, {bool Function(ApiException)? onError}){

  if(onError?.call(exception) == true){
    return true;
  }

  if(exception.code == 401 ){
    ///todo to login
    return true;
  }
  showError(exception.message ?? ApiException.unknownException);

  return false;
}

request 方法有个 block 函数参数,在 request 中进行调用,并对其包裹 try-catch ,在 catch 中进行统一异常处理,当外部未处理异常时则在 handleException 中进行统一处理,如 401 则跳转登录页,其他错误统一弹出错误提示。

此时使用如下:

代码语言:javascript
复制
 void testRequest() => request(() async {
    UserEntity? user = await apiService.test();
    print(user?.name);

    user = await apiService.test();
    print(user?.name);
  });

当 request 包裹的代码中其中一个请求错误则不会继续向下执行。

请求拦截

dio 支持添加拦截器自定义处理请求和返回数据,只需实现自定义拦截类继承 Interceptor 实现 onRequestonResponse 即可。

比如当登录后需要给所有请求添加统一的 Header 携带 token 信息时就可以通过拦截器实现。

代码语言:javascript
复制
class TokenInterceptor extends Interceptor{

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    ///token from cache
    var token = Cache.getToken();
    options.headers["Authorization"] = "Basic $token";
    super.onRequest(options, handler);
  }

  @override
  void onResponse(dio.Response response, ResponseInterceptorHandler handler) {
    super.onResponse(response, handler);
  }
}

然后在初始化 dio 时添加拦截器即可:

代码语言:javascript
复制
_dio.interceptors.add(TokenInterceptor());

日志打印

开发过程中为了方便调试经常需要打印请求返回日志,可以使用自定义拦截器实现,也可以使用第三方实现的日志打印的拦截器 pretty_dio_logger 库。

添加依赖:

代码语言:javascript
复制
pretty_dio_logger: ^1.1.1

dio 添加日期拦截器:

代码语言:javascript
复制
_dio.interceptors.add(PrettyDioLogger(requestHeader: true, requestBody: 
true, responseHeader: true));

PrettyDioLogger 拦截器可以设置打印哪些信息,可根据需求进行设置。

打印效果:

代码语言:javascript
复制
flutter: ╔╣ Request ║ POST
flutter: ║  https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/test
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Headers
flutter: ╟ content-type: application/json; charset=utf-8
flutter: ╟ Authorization: Basic ZHhtaF93ZWI6ZHhtaF93ZWJfc2VjcmV0
flutter: ╟ token: Bearer
flutter: ╟ contentType: application/json; charset=utf-8
flutter: ╟ responseType: ResponseType.json
flutter: ╟ followRedirects: true
flutter: ╟ connectTimeout: 15000
flutter: ╟ receiveTimeout: 0
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter:
flutter: ╔╣ Response ║ POST ║ Status: 200 OK
flutter: ║  https://www.fastmock.site/mock/6d5084df89b4c7a49b28052a0f51c29a/test/test
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Headers
flutter: ╟ access-control-allow-credentials: [true]
flutter: ╟ connection: [keep-alive]
flutter: ╟ x-powered-by: [Express]
flutter: ╟ set-cookie:
flutter: ║ [connect.sid=s%3AkDiyUQw5crHmB0UuY03dYX3Z2HPVO8Sf.bOVO2aDh%2FSviB70e9Xt5sMQjkiDtorwn%2B%2F
flutter: ║ bKN7y8UtY; Path=/; Expires=Sun, 06 Feb 2022 21:37:08 GMT; HttpOnly]
flutter: ╟ date: [Sun, 06 Feb 2022 09:37:08 GMT]
flutter: ╟ vary: [Accept, Origin, Accept-Encoding]
flutter: ╟ content-length: [82]
flutter: ╟ etag: [W/"52-2tuUsqqRy8jX+vcUJL+3D5AmQss"]
flutter: ╟ content-type: [application/json; charset=utf-8]
flutter: ╟ server: [nginx/1.17.8]
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Body
flutter: ║
flutter: ║    {
flutter: ║         code: 200,
flutter: ║         message: "success",
flutter: ║         data: {id: 111111, name: zhangsan, age: 18}
flutter: ║    }
flutter: ║
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝

loading 显示

网络请求是一个耗时操作,为了提高用户体验,一般会在请求的过程中显示 loading 提示用户正在加载数据。

前面解决异常处理使用了一个全局的 request 方法,loading 可以使用同样的思路实现,创建 loading 方法:

代码语言:javascript
复制
Future loading( Function block, {bool isShowLoading = true}) async{
  if (isShowLoading) {
    showLoading();
  }
  try {
    await block();
  } catch (e) {
    rethrow;
  } finally {
    dismissLoading();
  }
  return;
}

void showLoading(){
  EasyLoading.show(status: "加载中...");
}

void dismissLoading(){
  EasyLoading.dismiss();
}

实现很简单,在 block 调用前后调用 loading 的 show 和 dismiss。同时对 block 包裹 try-catch 保证在异常时取消 loading,并且在 catch 中不做任何处理直接抛出异常。

这里 loading 使用了 flutter_easyloading 插件

对 request 方法进行改造支持 loading :

代码语言:javascript
复制
Future request(Function() block,  {bool showLoading = true, bool Function(ApiException)? onError, }) async{
  try {
    await loading(block, isShowLoading:  showLoading);
  } catch (e) {
    handleException(ApiException.from(e), onError: onError);
  }
  return;
}

对 request 中的 block 又包装了一层 loading 从而实现自动 loading 的显示隐藏。

使用示例

经过上述步骤就完成了对网络请求的封装,接下来看看怎么使用。

开发过程中常用的网络请求为 get 和 post,为了方便调用,在 RequestClient 中添加 get 和 post 方法,如下:

代码语言:javascript
复制
Future<T?> get<T>(
    String url, {
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? headers,
    bool showLoading = true,
    bool Function(ApiException)? onError,
  }) {
    return request(url,
        queryParameters: queryParameters,
        headers: headers,
        onError: onError);
  }

  Future<T?> post<T>(
    String url, {
    Map<String, dynamic>? queryParameters,
    data,
    Map<String, dynamic>? headers,
    bool showLoading = true,
    bool Function(ApiException)? onError,
  }) {
    return request(url,
        method: "POST",
        queryParameters: queryParameters,
        data: data,
        headers: headers,
        onError: onError);
  }

实际也是封装后调用 request 方法。

基本使用

代码语言:javascript
复制
void login(String password) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = password;
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params);
    state.user = user;
    update();
  });


/// View
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    // Text("${SR.hello.tr} : ${state.count}", style: TextStyle(fontSize: 50.sp),),
    ElevatedButton(onPressed: () => controller.login("123456"), child: const Text("正常登录")),
    ElevatedButton(onPressed: () => controller.login("654321"), child: const Text("错误登录")),
    Text("登录用户:${state.user?.username ?? ""}", style: TextStyle(fontSize: 20.sp),),

  ],
)

自定义异常处理

代码语言:javascript
复制
void loginError(bool errorHandler) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "654321";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params);
    state.user = user;
       print("-------------${user?.username ?? "登录失败"}");
    update();
  }, onError: (e){
    state.errorMessage = "request error : ${e.message}";
    print(state.errorMessage);
    update();
    return errorHandler;
  });

onError 无论返回 false 或者 true 都会调用 onError 方法,且 print("-------------${user?.username ?? "登录失败"}"); 这句输出并没有执行,当 onError 返回 false 时依然会弹出错误的提示,是因为返回 false 时调用了默认的异常处理弹出提示,返回 true 时则不会调用默认的异常处理方法。

在 requestClient 的请求方法上添加 onError 处理是一样的效果,不同的是在 requestClient 上的 onError 为 true 时,下面的代码会正常执行:

代码语言:javascript
复制
 void loginError(bool errorHandler) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "654321";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params,  onError: (e){
      state.errorMessage = "request error : ${e.message}";
      print(state.errorMessage);
      update();
      return errorHandler;
    });
    state.user = user;
    print("-------------${user?.username ?? "登录失败"}");
    update();
  });

界面效果跟上面的一样,当 onError 返回 true 时,requestClient 下面的代码会正常执行。即会打印出 -------------登录失败, 返回 false 时则不会执行下面的代码。

loading 显示隐藏

代码语言:javascript
复制
void loginLoading(bool showLoading) => request(() async {
    LoginParams params = LoginParams();
    params.username = "loongwind";
    params.password = "123456";
    UserEntity? user = await requestClient.post<UserEntity>(APIS.login, data: params,  );
    state.user = user;
    update();
  }, showLoading: showLoading);

切换接口地址

在开发过程中会出现多个环境地址,比如开发环境、测试环境、预发布环境、生产环境等,此时为了方便切换环境一般都会在开发时增加一个环境切换的功能,此时就可以修改 baseUrl 然后重新创建 RequestClient 来实现。代码如下:

代码语言:javascript
复制
RequestConfig.baseUrl = "https://xxxxxx";
requestClient = RequestClient();

源码:flutter_app_core[1]

References

[1] flutter_app_core: https://github.com/loongwind/flutter_app_core

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 loongwind 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 添加依赖
  • 请求封装
  • 数据解析
    • 返回数据解析
      • 请求数据转换
      • 异常处理
      • 请求拦截
      • 日志打印
      • loading 显示
      • 使用示例
        • 基本使用
          • 自定义异常处理
            • loading 显示隐藏
              • 切换接口地址
                • References
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档