Flutter圈子flutter_学习资料

跟我学企业级flutter项目:如何用dio封装一套企业级可扩展

2022-02-28  本文已影响0人  王二蛋和他的狗

前言

网上有很多,比如说“Flutter Dio 亲妈级别封装教程”这篇文章,该文章上有几点问题:

  1. 重试机制代码错误
  2. token存取耦合很高
  3. 网络请求只能针对单一地址进行访问
  4. 网络请求缓存机制也不是很完美。

一旦依照这样的封装去做,那么项目后期的扩展性和易用性会有一定的阻碍,那么如何做到token存取无耦合,而且还能让app多种网络地址一同请求,还可以做到针对不同请求不同超时时长处理,网络缓存还加入可自动清理的lru算法呢?那么今天这篇文章为你揭晓企业级flutter dio网络层封装。

搭建前夕准备

三方库:

dio_cache_interceptor lru缓存库
dio 网络库
retrofit 网络生成库
connectivity_plus 网络情况判断

技能:

单例模式
享元模式
迭代

文章:

持久化:跟我学企业级flutter项目:dio网络框架增加公共请求参数&header

准备好如上技能,我们来封装一套优秀的网络层

一、准备好几个基本拦截器

1、超时拦截器

import 'dart:collection';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/tools/net/cache_object.dart';

class TimeInterceptor extends Interceptor {

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    Map<String, dynamic> extra = options.extra;
    bool connect = extra.containsKey(SysConfig.connectTimeout);
    bool receive = extra.containsKey(SysConfig.receiveTimeOut);
    if(connect||receive){
      if(connect){
        int connectTimeout = options.extra[SysConfig.connectTimeout];
        options.connectTimeout = connectTimeout;
      }
      if(receive){
        int receiveTimeOut = options.extra[SysConfig.receiveTimeOut];
        options.receiveTimeout = receiveTimeOut;
      }
    }
    super.onRequest(options, handler);

  }

}

作用:单独针对个别接口进行超时时长设定,如(下载,长链接接口)

2、缓存拦截器

dio_cache_interceptor 这个库中有lru算法缓存拦截库,可直接集成

3、持久化拦截器

跟我学企业级flutter项目:dio网络框架增加公共请求参数&header 本篇文章介绍了如何持久化

4、重试拦截器


import 'dart:async';
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/application.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';

import '../dio_utli.dart';

/// 重试拦截器
class RetryOnConnectionChangeInterceptor extends Interceptor {
  Dio? dio;

  RequestInterceptorHandler? mHandler;
  // RetryOnConnectionChangeInterceptor(){
  //
  // }

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    mHandler = handler;
    super.onRequest(options, handler);
  }


  @override
  Future onError(DioError err, ErrorInterceptorHandler handler) async{
    if (dio!=null&&Application.config.httpConfig.retry&&await _shouldRetry(err)) {
      return await retryLoop(err,handler,1);
    }
    return super.onError(err, handler);
  }

  Future retryLoop(DioError err, ErrorInterceptorHandler handler,int retry) async {
    try {
      ULog.d("${err.requestOptions.uri.toString()} retry : ${retry}",tag: "${SysConfig.libNetTag}Retry");
      await retryHttp(err,handler);
    } on DioError catch (err) {
      if(await _shouldRetry(err)&&retry<Application.config.httpConfig.retryCount){
        await retryLoop(err,handler,retry+1);
      }else{
        return super.onError(err, handler);
      }
    }
  }

  Future retryHttp(DioError err, ErrorInterceptorHandler handler) async {
    RequestOptions requestOptions = err.requestOptions;
    Options options = Options(
      method: requestOptions.method,
      sendTimeout: requestOptions.sendTimeout,
      receiveTimeout: requestOptions.receiveTimeout,
      extra: requestOptions.extra,
      headers: requestOptions.headers,
      responseType: requestOptions.responseType,
      contentType: requestOptions.contentType,
      validateStatus: requestOptions.validateStatus,
      receiveDataWhenStatusError: requestOptions.receiveDataWhenStatusError,
      followRedirects: requestOptions.followRedirects,
      maxRedirects: requestOptions.maxRedirects,
      requestEncoder: requestOptions.requestEncoder,
      responseDecoder: requestOptions.responseDecoder,
      listFormat: requestOptions.listFormat,
    );

    var res = await dio?.request(
      requestOptions.path,
      cancelToken: requestOptions.cancelToken,
      data: requestOptions.data,
      onReceiveProgress: requestOptions.onReceiveProgress,
      onSendProgress: requestOptions.onSendProgress,
      queryParameters: requestOptions.queryParameters,
      options: options,
    );

    return handler.resolve(res!);
  }

  ///要重试的类型
  Future<bool> _shouldRetry(DioError err) async{
    return err.error != null && err.error is SocketException && await isConnected();
  }

  Future<bool> isConnected() async {
    var connectivityResult = await (Connectivity().checkConnectivity());
    return connectivityResult != ConnectivityResult.none;
  }
}

该重试拦截器与其他文章封装不同,主要是用重试次数来管理重试机制。

5、日志拦截器


import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
typedef void LibLogPrint(String message);
class LibLogInterceptor extends Interceptor {
  LibLogInterceptor({
    this.request = true,
    this.requestHeader = true,
    this.requestBody = false,
    this.responseHeader = true,
    this.responseBody = false,
    this.error = true
  });

  /// Print request [Options]
  bool request;

  /// Print request header [Options.headers]
  bool requestHeader;

  /// Print request data [Options.data]
  bool requestBody;

  /// Print [Response.data]
  bool responseBody;

  /// Print [Response.headers]
  bool responseHeader;

  /// Print error message
  bool error;

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    var builder = StringBuffer('*** Request *** \n');
    builder.write(_printKV('uri', options.uri));
    //options.headers;

    if (request) {
      builder.write(_printKV('method', options.method));
      builder.write(_printKV('responseType', options.responseType.toString()));
      builder.write(_printKV('followRedirects', options.followRedirects));
      builder.write(_printKV('connectTimeout', options.connectTimeout));
      builder.write(_printKV('sendTimeout', options.sendTimeout));
      builder.write(_printKV('receiveTimeout', options.receiveTimeout));
      builder.write(_printKV(
          'receiveDataWhenStatusError', options.receiveDataWhenStatusError));
          builder.write(_printKV('extra', options.extra));
    }
    if (requestHeader) {
      builder.write('headers:\n');
      options.headers.forEach((key, v) => builder.write(_printKV(' $key', v)));
    }
    if (requestBody) {
      var res = options.data;
      builder.write('data:\n');
      builder.write(_message(res));
      // try{
      //   ULog.json(res.toString(),tag: "${SysConfig.libNetTag}RequestJson");
      // } on Exception catch (e) {
      //   ULog.d(res,tag: "${SysConfig.libNetTag}RequestJson");
      // }
    }
    ULog.d(builder.toString(),tag: "${SysConfig.libNetTag}Request");
    handler.next(options);
  }

  // Handles any object that is causing JsonEncoder() problems
  Object toEncodableFallback(dynamic object) {
    return object.toString();
  }

  String _message(dynamic res) {
    if (res is Map || res is Iterable) {
      var encoder = JsonEncoder.withIndent('  ', toEncodableFallback);
      return encoder.convert(res);
    } else {
      return res.toString();
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    var builder = StringBuffer('*** Response *** \n');
    _printResponse(response,builder,(message){
      ULog.d(message,tag: "${SysConfig.libNetTag}Response");
    });
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (error) {
      var builder = StringBuffer('*** DioError *** \n');
      builder.write('uri: ${err.requestOptions.uri}\n');
      builder.write('$err');
      if (err.response != null) {
        _printResponse(err.response!,builder,(message){
          ULog.e(message,tag: "${SysConfig.libNetTag}Error");
        });
      }else{
        ULog.e(builder.toString(),tag: "${SysConfig.libNetTag}Error");
      }
    }

    handler.next(err);
  }

  void _printResponse(Response response,StringBuffer builder,LibLogPrint pr) {
    builder.write(_printKV('uri', response.requestOptions.uri));
    if (responseHeader) {
      builder.write(_printKV('statusCode', response.statusCode));
      if (response.isRedirect == true) {
        builder.write(_printKV('redirect', response.realUri));
      }

      builder.write('headers:\n');
      response.headers.forEach((key, v) => builder.write(_printKV(' $key', v.join('\r\n\t'))));
    }
    if (responseBody) {
      var res = response.toString();
      builder.write('Response Text:\r\n');
      var resJ = res.trim();
      if (resJ.startsWith("{")) {
        Map<String, dynamic> decode = JsonCodec().decode(resJ);
        builder.write(_message(decode));
      }else if (resJ.startsWith("[")) {
        List decode = JsonCodec().decode(resJ);
        builder.write(_message(decode));
      }else {
        builder.write(res);
      }

      // try{
      //   ULog.json(res,tag: "${SysConfig.libNetTag}ResponseJson");
      // } on Exception catch (e) {
      //   ULog.d(res,tag: "${SysConfig.libNetTag}ResponseJson");
      // }
    }
    pr(builder.toString());

  }

  String _printKV(String key, Object? v) {
    return '$key: $v \n';
  }

}

在这里插入图片描述

主要是日志拦截打印

6、错误拦截器


import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/exception/lib_network_exception.dart';
import 'package:flutter_base_lib/src/tools/net/dio_utli.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

/// 错误处理拦截器
class ErrorInterceptor extends Interceptor {
// 是否有网
  Future<bool> isConnected() async {
    var connectivityResult = await (Connectivity().checkConnectivity());
    return connectivityResult != ConnectivityResult.none;
  }
  @override
  Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.type == DioErrorType.other) {
      bool isConnectNetWork = await isConnected();
      if (!isConnectNetWork && err.error is SocketException) {
        err.error = SocketException(LibLocalizations.getLibString().libNetWorkNoConnect!);
      }else if (err.error is SocketException){
        err.error = SocketException(LibLocalizations.getLibString().libNetWorkError!);
      }
    }
    err.error = LibNetWorkException.create(err);
    ULog.d('DioError : ${err.error.toString()}',tag: "${SysConfig.libNetTag}Interceptor");
    super.onError(err, handler);
  }

}

与其他人封装不同,服务器请求异常code,我将其抛到业务层自主处理。常规异常则走库文案。


import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

class LibNetWorkException implements Exception{

  final String _message;
  final int _code;

  int get code{
    return _code;
  }

  String get message{
    return _message;
  }

  LibNetWorkException( this._code,this._message);

  @override
  String toString() {
    return "$_code : $_message";
  }



  factory LibNetWorkException.create(DioError error) {
    switch (error.type) {
      case DioErrorType.cancel:{
          return LibNetWorkException(-1, LibLocalizations.getLibString().libNetRequestCancel!);
        }
      case DioErrorType.connectTimeout:{
          return LibNetWorkException(-1, LibLocalizations.getLibString().libNetFailCheck!);
        }
      case DioErrorType.sendTimeout:{
          return LibNetWorkException(-1, LibLocalizations.getLibString().libNetTimeOutCheck!);
        }
      case DioErrorType.receiveTimeout:{
          return LibNetWorkException(-1, LibLocalizations.getLibString().libNetResponseTimeOut!);
        }
      case DioErrorType.response:{
          try{
            return LibNetWorkException(error.response!.statusCode!,"HTTP ${error.response!.statusCode!}:${LibLocalizations.getLibString().libNetServerError!}");
          } on Exception catch (_) {
            return LibNetWorkException(-1, error.error.message);
          }
        }
      default:
        {
          return LibNetWorkException(-1, error.error.message);
        }
    }
  }
}

二、工具类封装

1、主要类



import 'dart:io';

import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/error_interceptor.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/lib_log_interceptor.dart';

import '../../../flutter_base_lib.dart';
import 'interceptor/presistent_interceptor.dart';
import 'interceptor/retry_on_connection_change_interceptor.dart';
import 'interceptor/time_interceptor.dart';

class DioUtil{

  final String _baseUrl;
  final HttpConfig _config;
  final List<Interceptor> _interceptors;

  late Dio _dio;

  Dio get dio{
    return _dio;
  }
  DioUtil._internal(this._baseUrl, this._config, this._interceptors){
    BaseOptions options = new BaseOptions(
      baseUrl: _baseUrl,
      connectTimeout: _config.connectTimeout,
      receiveTimeout: _config.receiveTimeOut,
    );
    _dio = new Dio(options);
    var retry = new Dio(options);
    _interceptors.forEach((element) {
      if(element is RetryOnConnectionChangeInterceptor){
        element.dio = retry;
      }else{
        if(!(element is ErrorInterceptor)){
          retry.interceptors.add(element);
        }
      }
      _dio.interceptors.add(element);
    });
    proxy(_dio);
    proxy(retry);
  }

  void proxy(Dio dio){
    if (SpSotre.instance.getBool(SysConfig.PROXY_ENABLE)??false) {
      String? porxy = SpSotre.instance.getString(SysConfig.PROXY_IP_PROT)??null;
      if(porxy!=null){
        (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
            (client) {
          client.findProxy = (uri) {
            return "PROXY $porxy";
          };
          //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
          client.badCertificateCallback =
              (X509Certificate cert, String host, int port) => true;
        };
      }
    }
  }

  static late Map<String,DioUtil> _dioUtils = Map();

  static DioUtil instance(String baseUrl,{HttpConfig? config, List<Interceptor>? interceptors,List<Interceptor>? applyInterceptors}){
    if(!_dioUtils.containsKey(baseUrl)){
      List<Interceptor> list = [PresistentInterceptor(),TimeInterceptor(),RetryOnConnectionChangeInterceptor(),LibLogInterceptor(requestBody: Application.config.debugState,responseBody: Application.config.debugState),ErrorInterceptor()];
      // List<Interceptor> list = [ErrorInterceptor(),PresistentInterceptor()];
      var inter = interceptors??list;
      if(applyInterceptors!=null){
        inter.addAll(applyInterceptors);
      }
      _dioUtils[baseUrl] = DioUtil._internal(baseUrl,config??Application.config.httpConfig,inter);
    }
    return _dioUtils[baseUrl]!;
  }

  // CancelToken _cancelToken = new CancelToken();


}

工具类封装,主要运用享元模式,可以支持多种url进行访问,不同的url有不同的配置。(灵活可用)

2、辅助类:



class HttpConfig{
  final int _connectTimeout ;
  final int _receiveTimeOut ;
  final bool _retry;
  final int _retryCount;

  get connectTimeout{
    return _connectTimeout;
  }

  get receiveTimeOut{
    return _receiveTimeOut;
  }

  get retry{
    return _retry;
  }
  get retryCount{
    return _retryCount;
  }

  HttpConfig(HttpConfigBuilder builder): _connectTimeout = builder._connectTimeout,_receiveTimeOut = builder._receiveTimeOut,_retry = builder._retry,_retryCount = builder._retryCount;
}

class HttpConfigBuilder {
  int _connectTimeout = 10000;//连接超时时间
  int _receiveTimeOut = 30000;//接收超时时间
  bool _retry = false;
  int _retryCount = 3;

  // var maxRetry = 1 重试次数

  HttpConfigBuilder setConnectTimeout(int connectTimeout){
    _connectTimeout = connectTimeout;
    return this;
  }

  HttpConfigBuilder setReceiveTimeOut(int receiveTimeOut){
    _receiveTimeOut = receiveTimeOut;
    return this;
  }

  HttpConfigBuilder setRetry(bool retry){
    _retry = retry;
    return this;
  }

  HttpConfigBuilder setRetryCount(int retryCount){
    _retryCount = _retryCount;
    return this;
  }

  HttpConfig build() => HttpConfig(this);
}

三、使用


import 'package:flutter_app_me/data/model/api_result.dart';
import 'package:flutter_app_me/data/model/user.dart';
import 'package:flutter_app_me/data/model/user_infos.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';

import 'api_methods.dart';

part 'api_service.g.dart';

@RestApi()
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  @GET(ApiMethods.userinfoJson)
  Future<ApiResult<UserInfos>> userinfoJson();

  // "test123332","123456"
  @POST(ApiMethods.login)
  @Extra({SysConfig.connectTimeout:100000})
  Future<ApiResult<UserInfos>> userLogin(@Queries() User user);
}

网络请求配置




class BusinessErrorException implements Exception {
  final int _errorCode;
  final String? _errorMsg;

  BusinessErrorException(this._errorCode, this._errorMsg);

  int get errorCode {
    return _errorCode;
  }

  String? get errorMsg => _errorMsg;
}


class TokenTimeOutException implements Exception {
  final String? _errorMsg;
  TokenTimeOutException(this._errorMsg);
  String? get errorMsg => _errorMsg;

}

class RequestCodeErrorException implements Exception {
  final String? _errorMsg;
  final int _errorCode;
  RequestCodeErrorException(this._errorCode, this._errorMsg);

  int get errorCode {
    return _errorCode;
  }

  String? get errorMsg => _errorMsg;
}

业务基本异常


import 'package:business_package_auth/business_package_auth.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:dio/dio.dart';
import 'package:wisdomwork_lib/src/model/api_result.dart';

const int httpSuccessCode = 0;
const int httpErrorCode = 1;
const int httpTokenExt = 10001;

extension SuccessExt<T> on Success<T> {
  Success<T> appSuccess() {
    var data = this.data;
    if (data is ApiResult) {
      if (data.code != httpSuccessCode) {
        switch (data.code){
          case httpTokenExt:
            TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessTokenTimeOut!,tipType: TipType.warning);
            BlocProvider.of<AuthenticationBloc>(LibRouteNavigatorObserver.instance.navigator!.context).add(LogOut());
            throw TokenTimeOutException(data.msg);
          case httpErrorCode:
            TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessRequestCodeError!,tipType: TipType.error);
            throw RequestCodeErrorException(data.code!,data.msg);
          default:
            throw BusinessErrorException(data.code!, data.msg);
        }

      }
    }
    return this;
  }
}

extension ErrorExt<T> on Error<T> {
  void appError() {
    var exception = this.exception;
    if (exception is LibNetWorkException) {
      TipToast.instance.tip(exception.message, tipType: TipType.error);
    }
  }
}


typedef ResultF<T> = Future<ApiResult<T>> Function();

mixin RemoteBase {

  Future<DataResult<ApiResult<T>>> remoteDataResult<T>(ResultF<T> resultF) async {
    try {
      var data = await resultF.call();
      return Success(data).appSuccess();
    } on DioError catch (err, stack) {
      var e = err.error;
      ULog.e(e.toString(), error: e, stackTrace: stack);
      return Error(e)..appError();
    } on Exception catch (e, stack) {
      ULog.e(e.toString(), error: e, stackTrace: stack);
      return Error(e)..appError();
    }
  }

}

业务基本异常处理方式


import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:wisdomwork/data/services/api_service.dart';
import 'package:wisdomwork_lib/wisdomwork_lib.dart';

mixin WisdomworkRemoteBase{
  var rest = RestClient(DioUtil.instance(
      AppEnvironment.envConfig![AppConfig.apiName]!,
      applyInterceptors: [UiNetInterceptor()]).dio);
}

业务请求接口,实现

     final data = await AppResponsitory.instance.login(state.phoneText, state.codeText);
     if (userResult != null) {
                if (userResult is Success) {
                  if (userResult.data!.data!= null) {
                    onGetUser(userResult.data!.data!, context);
                  }
                } else if(userResult is Error){
                  var exception = (userResult as Error).exception;
                  if(exception is BusinessErrorException){
                    Fluttertoast.showToast(msg: exception.errorMsg.toString());
                  }
                }
              }

业务请求与异常处理

上一篇下一篇

猜你喜欢

热点阅读