Flutter 网络封装 2022-10-12 周三

网络选择

  • Flutter自带httpClient,这个也是很好用的;

  • Http库,有个三方库的名字就叫这个;

  • Dio,这是目前最热门的,相当于iOS中AFNetworking。随大流,就选这个进行封装。

Dio引入

  • Dio是一个第三方库,所以需要先下载。使用一行命令就可以引入flutter pub add dio
    dio: ^4.0.6

  • 日志是需要的,最简单的就是用系统提供debugPrint,基本上也够用了。为Dio专门写的插件也有,比如dio_logger。也有比较流行的插件,比如logger

  • loading一方面是等待,另一方是防止用户误操作。一般这个也是用第三方插件的居多。
    这方面有一个比较突出的第三方插件,那就用吧。
    flutter_easyloading: ^3.0.5
    另外,toast一般和loading都在一个插件中,这两种都是需要的,类似的插件也可以考虑
    bot_toast
    感觉上flutter_easyloading要好一点。

  • 网络状态检测,主要是判断有网还是没网,功能和ping类似,这个也一般需要第三方插件。如果没有这个,只有等Dio连接断了再提示。这方面也有一个比较流行的第三方插件。
    connectivity_plus: ^2.3.6
    当然,监听网络状态也是可以的,不过这个不是强需求,可以再需要的时候添加。

  • 网络缓存,失败重传,cookie管理这些,不是必须的需求,可以延后考虑。目前这些也有相应的Dio配套三方库可以选择。这些都是拦截器模式的,可以随时添加。

  • 抓包,代理,证书验证这些也暂时不加,等以后有需要的时候再考虑。

连接常数

用一个类来保存网络连接需要常数,目前主要是超时时间,baseURL,头部信息等。常见的需求切换后台环境,主要的就是更换这个baseURL

class HttpOptions {
  /// 超时时间;单位是ms
  static const int connectTimeout = 30000;
  static const int receiveTimeout = 30000;

  /// 地址前缀
  static const String baseUrl = 'https://baidu.com';

  /// header
  static const Map<String, dynamic> headers = {
    'Accept': 'application/json,*/*',
    'Content-Type': 'application/json',
    'currency': 'CNY',
    'lang': 'en',
    'device': 'app',
  };
}

异常处理

  • 错误提示是重要的基础功能,绕不过的,所以自定义一个类,实现Exception协议,主要包括错误码和错误信息两部分内容。只需要一次封装就好了,分太细反而麻烦。

  • DioError是Dio提供的一个错误信息结构,我们可以基于这个,添加一些自定义的信息。

  • DioError中还包含服务响应Response,里面有代表服务强响应的状态码和错误信息,可以利用这点,对服务器端的错误状态进行一次封装。

import 'package:dio/dio.dart';

class HttpException implements Exception {
  final int code;
  final String msg;

  HttpException({
    this.code = -1,
    this.msg = 'unknow error',
  });

  @override
  String toString() {
    return 'Http Error [$code]: $msg';
  }

  factory HttpException.create(DioError error) {
    /// dio异常
    switch (error.type) {
      case DioErrorType.cancel:
        {
          return HttpException(code: -1, msg: 'request cancel');
        }
      case DioErrorType.connectTimeout:
        {
          return HttpException(code: -1, msg: 'connect timeout');
        }
      case DioErrorType.sendTimeout:
        {
          return HttpException(code: -1, msg: 'send timeout');
        }
      case DioErrorType.receiveTimeout:
        {
          return HttpException(code: -1, msg: 'receive timeout');
        }
      case DioErrorType.response:
        {
          try {
            int statusCode = error.response?.statusCode ?? 0;
            // String errMsg = error.response.statusMessage;
            // return ErrorEntity(code: errCode, message: errMsg);
            switch (statusCode) {
              case 400:
                {
                  return HttpException(code: statusCode, msg: 'Request syntax error');
                }
              case 401:
                {
                  return HttpException(code: statusCode, msg: 'Without permission');
                }
              case 403:
                {
                  return HttpException(code: statusCode, msg: 'Server rejects execution');
                }
              case 404:
                {
                  return HttpException(code: statusCode, msg: 'Unable to connect to server');
                }
              case 405:
                {
                  return HttpException(code: statusCode, msg: 'The request method is disabled');
                }
              case 500:
                {
                  return HttpException(code: statusCode, msg: 'Server internal error');
                }
              case 502:
                {
                  return HttpException(code: statusCode, msg: 'Invalid request');
                }
              case 503:
                {
                  return HttpException(code: statusCode, msg: 'The server is down.');
                }
              case 505:
                {
                  return HttpException(code: statusCode, msg: 'HTTP requests are not supported');
                }
              default:
                {
                  return HttpException(
                      code: statusCode, msg: error.response?.statusMessage ?? 'unknow error');
                }
            }
          } on Exception catch (_) {
            return HttpException(code: -1, msg: 'unknow error');
          }
        }
      default:
        {
          return HttpException(code: -1, msg: error.message);
        }
    }
  }
}

异常拦截器

  • 把错误处理做一个拦截器,在onError方法中将自定义的异常类型放入DioError的error字段(dynamic的)

  • 这个确实有点绕。显示通过DioError的error type来创建自定义的异常类型HttpException;然后又把自定义的异常类型通过拦截器放入DioError的error字段,重新回归到Dio的框架处理过程之中。

  • 如果没有网络,DioError的type字段为DioErrorType.other,这部分自定义的异常类型HttpException并没有处理。所以这个时候,还需要调用一下检测网络状态第三方插件connectivity_plus,看看是不是断网了。

class ErrorInterceptor extends Interceptor {
  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    /// 根据DioError创建HttpException
    HttpException httpException = HttpException.create(err);

    /// dio默认的错误实例,如果是没有网络,只能得到一个未知错误,无法精准的得知是否是无网络的情况
    /// 这里对于断网的情况,给一个特殊的code和msg
    if (err.type == DioErrorType.other) {
      var connectivityResult = await (Connectivity().checkConnectivity());
      if (connectivityResult == ConnectivityResult.none) {
        httpException = HttpException(code: -100, msg: 'None Network.');
      }
    }

    /// 将自定义的HttpException
    err.error = httpException;

    /// 调用父类,回到dio框架
    super.onError(err, handler);
  }
}

封装

  • 一般都会把Dio封装为一个单例。一个APP中,一个Dio实例足够了。

  • 虽然Dio提供了get,post等各种方法,但是需要加入一些自定义的参数,所以一般会直接封装更底层的request方法。

  • http的method是字符串,可以考虑用一个枚举,在调用request方法的时候统一转换。

  • loading,错误信息,可以在这个request方法上统一用try catch结构在这里处理

  • 错误信息,log,自定义头部(比如token)等可以通过拦截器的形式加入。有很多配合Dio的拦截器第三方插件,比如pretty_dio_logger

  • 公共头部信息,超时时间,baseUrl等信息可以通过BaseOption的形式在Dio单例创建的时候给出。

  • 切换环境可以通过修改baseUrl的方式实现。直接代码注释是最方便的,高级一点的话,可以通过本地缓存的方式来实现。

class HttpRequest {
  // 单例模式使用Http类,
  static final HttpRequest _instance = HttpRequest._internal();
  factory HttpRequest() => _instance;

  static late final Dio dio;

  /// 内部构造方法
  HttpRequest._internal() {
    /// 初始化dio
    BaseOptions options = BaseOptions(
      connectTimeout: HttpOptions.connectTimeout,
      receiveTimeout: HttpOptions.receiveTimeout,
      sendTimeout: HttpOptions.sendTimeout,
      baseUrl: HttpOptions.baseUrl,
      headers: HttpOptions.headers,
    );
    dio = Dio(options);

    /// 添加各种拦截器
    dio.interceptors.add(ErrorInterceptor());
    dio.interceptors.add(dioLoggerInterceptor);
  }

  /// 封装request方法
  Future request({
    required String path,
    required HttpMethod method,
    dynamic data,
    Map<String, dynamic>? queryParameters,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) async {
    const Map methodValues = {
      HttpMethod.get: 'get',
      HttpMethod.post: 'post',
      HttpMethod.put: 'put',
      HttpMethod.delete: 'delete',
      HttpMethod.patch: 'patch',
      HttpMethod.head: 'head'
    };
    Options options = Options(
      method: methodValues[method],
    );

    try {
      if (showLoading) {
        EasyLoading.show(status: 'loading...');
      }
      Response response = await HttpRequest.dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      return response.data;
    } on DioError catch (error) {
      HttpException httpException = error.error;
      if (showErrorMessage) {
        EasyLoading.showToast(httpException.msg);
      }
    } finally {
      if (showLoading) {
        EasyLoading.dismiss();
      }
    }
  }
}

enum HttpMethod {
  get,
  post,
  delete,
  put,
  patch,
  head,
}

工具方法

  • 直接使用request方法不是很方便,所以,再封装一层,对外仍然提供get,post等方便方法

  • 一般直接包装成静态方法,用起来最方便。

/// 调用底层的request,重新提供get,post等方便方法
class HttpUtil {
  static HttpRequest httpRequest = HttpRequest();

  /// get
  static Future get({
    required String path,
    Map<String, dynamic>? queryParameters,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) {
    return httpRequest.request(
      path: path,
      method: HttpMethod.get,
      queryParameters: queryParameters,
      showLoading: showLoading,
      showErrorMessage: showErrorMessage,
    );
  }

  /// post
  static Future post({
    required String path,
    required HttpMethod method,
    dynamic data,
    bool showLoading = true,
    bool showErrorMessage = true,
  }) {
    return httpRequest.request(
      path: path,
      method: HttpMethod.post,
      data: data,
      showLoading: showLoading,
      showErrorMessage: showErrorMessage,
    );
  }

逻辑模块

  • 一般后台会按照逻辑模块分类,分类一般以path中的字符来区分。比如用户模块一般以/user/开头。

  • 对应于后台的习惯,可以考虑以模块名为文件名,将类似的接口放在同一个文件中。比如用户模块都放在user_api.dart文件中。

  • 具体到每一个接口,path,参数等都可以确定,所以在用static方法包一层是可行的。

import 'package:panda_buy/apis/http/http_util.dart';

class UserApi {
  /// path定义
  static const String pathPrefix = '/gateway/user/';

  /// 获取公钥
  static Future getPubKey() {
    return HttpUtil.get(
      path: '${pathPrefix}pubkey',
      showLoading: false,
      showErrorMessage: false,
    );
  }
  • 文件结果如下
企业微信截图_d8012040-f6af-4d98-a46c-1eabb2a05d95.png

log

  • dio_logger 是为Dio定制的,只要一行代码就可以了dio.interceptors.add(dioLoggerInterceptor);

  • log内容按照request和response分开,基本能用。

  • 颜色区分没有,不知道什么原因。

  • 以拦截器的形式作为Dio的配件引入,感觉还是不错的。

企业微信截图_1eb5734a-3e11-4ed1-8bc0-ef41b183451d.png

参考文章

Flutter Dio 亲妈级别封装教程

Flutter应用框架搭建(四) 网络请求封装

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

推荐阅读更多精彩内容