Flutter的Dio网络请求封装

一、添加依赖

在yaml文件里边添加如下依赖

dependencies:
  dio: ^4.0.4

二、添加配置文件

新建一个network_config.dart文件存放网络配置

class NetWorkConfig {
  static String baseUrl =
      "https://www.fastmock.site/mock/d8bfa0e1c67bfd329a33ae3294d98c78/test";
  static const connectTimeOut = 10000;
  static const readTimeOut = 10000;
  static const writeTimeOut = 10000;
  static const successCode = 200;
}

三、请求封装

ApiResponse是之前定义的公共接口返回实体Flutter的Json数据解析之FlutterJsonBeanFactory插件

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter_core/network/network_config.dart';

import '../models/api_response/api_response_entity.dart';
import '../models/api_response/raw_data.dart';
import 'exception.dart';

class RequestClient {
  late Dio _dio;
  static final RequestClient _singletonRequestClient =
      RequestClient._internal();

  factory RequestClient() {
    return _singletonRequestClient;
  }

  RequestClient._internal() {
    ///初始化 dio 配置
    var options = BaseOptions(
        baseUrl: NetWorkConfig.baseUrl,
        connectTimeout: NetWorkConfig.connectTimeOut,
        receiveTimeout: NetWorkConfig.readTimeOut,
        sendTimeout: NetWorkConfig.writeTimeOut);
    _dio = Dio(options);
  }

  /// dio 本身提供了get 、post 、put 、delete 等一系列 http 请求方法,最终都是调用request,直接封装request就行
  Future<T?> request<T>(
    String url, {
    required String method,
    Map<String, dynamic>? queryParameters,
    dynamic 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) {
      ///创建 ApiException ,调用 onError,当 onError 返回为 true 时即错误信息已被调用方处理,则不抛出异常,否则抛出异常。
      var exception = ApiException.from(e);
      if (onError?.call(exception) != true) {
        throw exception;
      }
    }
    return null;
  }

  ///将请求 data 数据先使用 jsonEncode 转换为字符串,再使用 jsonDecode 方法将字符串转换为 Map。
  _convertRequestData(data) {
    if (data != null) {
      data = jsonDecode(jsonEncode(data));
    }
    return data;
  }

  ///请求响应内容处理
  T? _handleResponse<T>(Response response) {
    if (response.statusCode == 200) {
      //判断泛型是否为 RawData ,是则直接把 response.data 放入 RawData 中返回,
      //即 RawData 的 value 就是接口返回的原始数据。用于第三方平台的接口请求,返回数据接口不支持定义的ApiResponse的情况
      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;
    }
  }

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

  Future<T?> get<T>(
    String url, {
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? headers,
    bool showLoading = true,
    bool Function(ApiException)? onError,
  }) {
    return request(url,
        method: "Get",
        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);
  }

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

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

}

四、异常处理

主要是对http异常和业务异常进行处理。

import 'package:dio/dio.dart';
import 'package:flutter_core/models/api_response/api_response_entity.dart';

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 {
          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);
}

五、嵌套请求

上述封装后,如果业务存在多个请求依赖调用,就需要统一的处理错误。

//先定义一个顶级的request
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;
}

//统一的错误处理
bool handleException(ApiException exception,
    {bool Function(ApiException)? onError}) {
  if (onError?.call(exception) == true) {
    return true;
  }
//当外部未处理异常时则在 handleException 中进行统一处理
  //todo 错误码统一处理
  // if(exception.code == 401 ){
  //
  //   return true;
  // }
  EasyLoading.showError(exception.message ?? ApiException.unknownException);
  return false;
}

//请求的dialog
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();
}

六、拦截器

Dio支持自定义拦截器,继承Interceptor,重写onRequestonResponse方法就行。

class HeadInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    super.onRequest(options, handler);
  }

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

在初始化dio的地方,把拦截器加入dio对象的拦截器集合dio.interceptors中就行。

七、日志打印

可以通过自定义的拦截器实现,也可以引入pretty_dio_logger库。

dependencies:
  pretty_dio_logger: ^1.1.1

_dio.interceptors.add(PrettyDioLogger(
    requestHeader: true,
    requestBody: true,
    responseHeader: true,
    responseBody: true));
image.png

八、模拟请求

fastmock上新建自己的项目,接口配置如下:

{
  code: ({
    _req,
    Mock
  }) => {
    let body = _req.body;
    return body.username === "qi" && body.password === "123456" ?
      200 :
      1000001;
  },
  message: ({
    _req,
    Mock
  }) => {
    let body = _req.body;
    return body.username === "qi" && body.password === "123456" ?
      "success" :
      "请确认账号密码后再次重试";
  },
  data: function({
    _req,
    Mock
  }) {
    let body = _req.body;
    if (body.username === "qi" && body.password === "123456") {
      return Mock.mock({
        username: "qi",
        userId: "123456",
        email: "@email",
        address: "@address",
        "age|10-30": 18,
      });
    } else {
      return null
    }
  },
};

发起请求:

  void login() =>
      request(() async {
        LoginParams params = LoginParams();
        params.username = "qi";
        params.password = "123456";
        UserEntity? user =
        await apiService.login(params);
        state.user = user;
        debugPrint("-------------${user?.username ?? "登录失败"}");
        update();
      }, onError: (ApiException e) {
        state.errorMessage = "request error : ${e.message}";
        debugPrint(state.errorMessage);
        update();
        return true;
      });

  void loginError() =>
      request(() async {
        LoginParams params = LoginParams();
        params.username = "qi";
        params.password = "123";
        UserEntity? user =
        await apiService.login(params);
        state.user = user;
        debugPrint("-------------${user?.username ?? "登录失败"}");
        update();
      },onError: (ApiException e) {
        state.errorMessage = "request error : ${e.message}";
        debugPrint(state.errorMessage);
        update();
        if (e.code == 1000001) {
          return false;
        } else {
          return true;
        }
      });

效果展示:


image.png

image.png

image.png

参考文章:
https://juejin.cn/post/7061806192980410382
https://juejin.cn/post/7043721908801503269

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容