Flutter Dio封装实践(3行代码实现http请求)

6,887 阅读4分钟

接着上一篇文章:基于MVVM架构封装Flutter基础库

这篇文章主要介绍dio封装,实现http请求:Demo地址

一、http组件XApi包含的特性

 1、http请求时传递泛型直接解析为对应对象或List; 

 2、配合MVVM组件(BaseViewModel和BaseView)使用,实现了通用加载中、加载错误页,空白页以及正常显示页UI和逻辑; 

 3、http加载时dialog自动显示隐藏逻辑,dialog UI可自定义; 

 4、http加载时手动关闭dialog会自动取消请求,也可以禁止在请求过程中关闭弹窗; 

 5、请求失败时自动显示错误Toast,可以自定义样式或不显示; 

 6、token失效时回调指定方法; 

 7、可以自定义加载提示文本;

二、初始化http组件

在应用启动时调用BaseLibPlugin.init()初始化httpConfig:

BaseLibPlugin.init(
        ...省略其它代码
        ///http配置
        httpConfig: HttpConfigImpl(),
        ...
);

 参考下面代码,根据业务实现IHttpConfig接口:

///@date:  2021/2/26 14:01
///@author:  lixu
///@description: 全局http相关配置
///当前配置在[XApi]类中被调用
class HttpConfigImpl implements IHttpConfig {
String _tag = 'HttpConfigImpl';

///配置默认值:http请求时是否显示加载dialog
@override
bool isShowLoading() {
    return true;
}

///配置默认值:http加载提示文本
@override
String configLoadingText() {
    return 'loading...';
}

///配置默认值:加载中能否通过关闭加载弹窗取消请求
@override
bool isCancelableDialog() {
    return false;
}

///配置默认值:请求失败时是否自动显示toast提示错误
@override
bool isShowFailToast() {
    return true;
}

///配置默认值:请求前是否校验网络连接
///true:如果无网络,直接返回错误
@override
bool isCheckNetwork() {
    return true;
}

///配置通用的http请求选项[BaseOptions]
///优先级最低,优先取[XApi]#[request]方法中配置的method和option
@override
BaseOptions configBaseOptions() {
    BaseOptions options = BaseOptions(
            baseUrl: HttpUrls.httpHost,
            connectTimeout: HttpConst.httpTimeOut,
            receiveTimeout: HttpConst.httpTimeOut,
            sendTimeout: HttpConst.httpTimeOut,
            contentType: XApi.contentTypeJson,
            method: XApi.methodPost,
            responseType: ResponseType.json,
         );
    return options;
}

///返回http成功的响应码
@override
String configHttpResultSuccessCode() {
    return HttpConst.httpResultSuccess.toString();
}

///配置https
@override
bool configHttps(X509Certificate cert, String host, int port) {
    ///TODO 根据业务做校验
    
    ///true:忽略证书校验
    return true;
}

///添加http拦截器
///拦截器队列的执行顺序是FIFO,先添加的拦截器先执行
@override
List<Interceptor> configInterceptors() {
    List<Interceptor> interceptors = [];
    interceptors.add(HeaderInterceptor());
    
    ///TODO 可以添加拦截器实现http缓存逻辑,或其它功能
    return interceptors;
}

///是否自动添加[LogInterceptors]默认日志拦截器,打印http请求响应相关的日志
@override
bool configLogEnable() {
    return true;
}

///每个http请求前回调该方法获取baseUrl
///优先级高于[IHttpConfig]#[configBaseOptions]方法配置的baseUrl
///[url] 当前正在请求的接口url
///return: 返回null使用[IHttpConfig]#[configBaseOptions]方法配置的baseUrl
@override
String getHttpHost(String url) {
    return HttpUrls.httpHost;
}

///http请求失败时会回调该方法,判断是否是token失效导致的错误
///[errorBean] 请求失败对象
@override
bool isHttpRespTokenError(HttpErrorBean errorBean) {
    ///通过code判断是否是token失效了
    return HttpConst.sysTokenError.toString() == errorBean.code 
            || HttpConst.sysTokenExpired.toString() == errorBean.code;
}

///token失效回调该方法
///[errorBean] 请求失败对象
@override
void onTokenErrorCallback(HttpErrorBean errorBean) {
  ToastUtils.show("Token 失效:${errorBean.toString()}", isShowLong: true);
  ///TODO 实现token失效的业务逻辑
  
}

///将http响应的json解析成对象
///[url] 当前请求url
///[jsonData] http响应完整json
///[isRespListData] 响应数据是否是List格式
@override
HttpResultBean<T> parseJsonToObject<T>(String url, Map<String, dynamic> 
jsonData, bool isRespListData) {
  ///TODO 通过传递的泛型,解析成对象
  return HttpJsonUtils.parseJsonToObject<T>(url, jsonData, isRespListData);
}

///http请求显示加载框
///[XApi]#[request]方法isShowLoading字段为true时,会回调该方法
///[url] 当前请求url
///[tag] 当前请求对应的tag,唯一
///[cancelToken] 用于加载框关闭时取消http请求
///[loadingText] 加载提示提示文本
///[isCancelableDialog] 请求过程中能否关闭加载框,默认false
@override
void showLoading(String url, int tag, CancelToken cancelToken, String loadingText, 
bool isCancelableDialog) {    LogUtils.i(_tag, 'showLoading tag:$tag  loadingText:$loadingText');

        ///显示http加载dialog:isShowLoading为true时,会回调该方法
        BaseLibPlugin.oneContext.showDialog(
                    barrierDismissible: false,
                    barrierColor: Colors.transparent,
                    isBackButtonDismissible: isCancelableDialog,
                    builder: (_) {
                        ///TODO 可以参考HttpLoadingDialog类自定义dialog样式
                        
                        return HttpLoadingDialog(loadingText);
                    },
                    onClickBackButtonDismissCallback: () {
                            ///请求过程中关闭加载框时取消请求
                            if (isCancelableDialog) {
                                  XApi().cancel(cancelToken);
                            }
                    },
         );
}

///http请求完成,关闭加载框
///[XApi]#[request]方法isShowLoading字段为true时,会回调该方法
///[url] 当前请求url
///[tag]当前请求对应的tag,唯一
@override
void hideLoading(String url, int tag, bool isCancelled) {
    if (!isCancelled && BaseLibPlugin.oneContext.hasDialogVisible) {
          BaseLibPlugin.oneContext.popDialog();
    }
}

三、XApi 主要方法说明

///@date:  2021/2/25 14:25
///@author:  lixu
///@description: 网络请求基类(单例)
class XApi {
    ...省略其它代码

///单例  
factory XApi() {
    if (_instance == null) {
        _instance = XApi._internal();
    }
    return _instance;
}

///私有构造方法  
XApi._internal() {
    ///基于通用BaseOptions创建单例Dio对象
    _dio = Dio(_httpConfig.configBaseOptions());
    
    ///拦截器队列的执行顺序是FIFO
    ///添加自定义拦截器
    if (_httpConfig.configInterceptors() != null 
            && _httpConfig.configInterceptors().isNotEmpty) {
        _dio.interceptors.addAll(_httpConfig.configInterceptors());
    }

    ///添加默认的日志拦截器
    if (_httpConfig.configLogEnable()) {
        _dio.interceptors.add(LogsInterceptors());
    }

    (_dio.httpClientAdapter as DefaultHttpClientAdapter).
        onHttpClientCreate = (client) {
            client.badCertificateCallback = 
                (X509Certificate cert, String host, int port) {
                    ///true:忽略证书校验
                    return _httpConfig.configHttps(
                                        cert,host,port) ?? true;
                };
    };
}   


///发起请求,响应data为单个对象
Future request<T>(String url,
{Map<String, dynamic> params,
    Map<String, dynamic> header,
    String method,
    RequestOptions option,
    bool isShowLoading,
    String loadingText,
    bool isCancelableDialog,
    bool isShowFailToast,
    bool isCheckNetwork,
    CancelToken cancelToken,
    Function(T) onSuccess,
    Function(HttpErrorBean) onError,
    Function() onComplete}
) {
    调用_commonRequest方法
}


///发起请求,响应data为List
Future requestList<T>(String url,
{Map<String, dynamic> params,
    Map<String, dynamic> header,
    String method,
    RequestOptions option,
    bool isShowLoading,
    String loadingText,
    bool isCancelableDialog,
    bool isShowFailToast,
    bool isCheckNetwork,
    CancelToken cancelToken,
    Function(List<T>) onSuccess,
    Function(HttpErrorBean) onError,
    Function() onComplete}
){
    调用_commonRequest方法
}

///通用http请求
///[url] 请求url
///[params] 请求参数,可为空
///[isResultList] 返回的data是否是List类型
///[header] 请求头
///[method] 请求方法,优先级最高
///[option] 针对当前请求的配置选项,优先级次高
///[isShowLoading] 是否显示加载弹窗
///[loadingText] 加载提示
///[isCancelableDialog] 加载中能否关闭加载弹窗
///[isShowFailToast] 请求失败时是否自动显示toast提示错误
///[isCheckNetwork] 请求前是否校验网络连接
///[onSuccessListCallback] 请求List成功回调
///[onSuccessObjCallback] 请求单个对象成功回调
///[onErrorCallback] 请求失败回调
///[onComplete] 请求完成回调,在onSuccess或onError方法后面调用
Future _commonRequest<T>(String url, bool isResultList,
        {Map<String, dynamic> params,
        Map<String, dynamic> header,
        String method,
        RequestOptions option,
        bool isShowLoading,
        String loadingText,
        bool isCancelableDialog,
        bool isShowFailToast,
        bool isCheckNetwork,
        CancelToken cancelToken,
        Function(List<T>) onSuccessListCallback,
        Function(T) onSuccessObjCallback,
        Function(HttpErrorBean) onErrorCallback,
        Function() onCompleteCallback}) async {
        
    ///设置默认值
    isShowLoading ??= _httpConfig.isShowLoading();
    loadingText ??= _httpConfig.configLoadingText();
    isCancelableDialog ??= _httpConfig.isCancelableDialog();
    isShowFailToast ??= _httpConfig.isShowFailToast();
    isCheckNetwork ??= _httpConfig.isCheckNetwork();

    if (isCheckNetwork) {
        ///判断网络连接
        ConnectivityResult connResult = 
                              await Connectivity().checkConnectivity();
        if (connResult != null && connResult == ConnectivityResult.none) {
            return _onRespErrorCallback(
                    isShowFailToast,
                    onErrorCallback,
                    HttpErrorBean(code: HttpCode.networkError?.toString(), 
                                  message: '无网络连接,请检查网络设置'),
                    );
         }
    }
    
    option ??= RequestOptions();
    
    ///添加baseUrl
    ///baseUrl优先级:形参option.baseUrl>_httpConfig.getBaseUrl>_httpConfig.
                                                                configBaseOptions
    if (!url.startsWith(Constants.httpStartWith) 
                        && option.baseUrl == null) {
        String baseUrl = _httpConfig.getBaseUrl(url);
        if (baseUrl != null && baseUrl.isNotEmpty) {
          option.baseUrl = baseUrl;
        }
    }

    params ??= {};
    
    ///添加CancelToken,用于取消请求
    cancelToken ??= CancelToken();
    _cancelTokenList ??= [];
    _cancelTokenList.add(cancelToken);
    
    ///处理请求头
    if (header != null) {
        option.headers ??= {};
        option.headers.addAll(header);
    }

    ///设置请求方法
    if (method != null) {
        option.method = method;
    } else {
        option.method ??= methodPost;
    }
    
    ///显示加载框
    if (isShowLoading) {
      ///只是封装了显示dialog的逻辑,具体的dialog UI实现交给调用者处理
       _httpConfig.showLoading(url, 
                               cancelToken.hashCode, 
                               cancelToken, 
                               loadingText, 
                               isCancelableDialog
                               );
    }

    try {
        Response response;
        if (methodGet == option.method) {
            ///get请求
            response = await _dio.get(url, 
                                      queryParameters: params, 
                                      options: option, 
                                      cancelToken: cancelToken
                                      );
        } else if (methodPost == option.method) {
            ///默认post请求
            response = await _dio.post(url, 
                                       data: params, 
                                       options: option, 
                                       cancelToken: cancelToken
                                       );
        } else {
            ///其他请求方式
            response = await _dio.request(url, 
                                          data: params, 
                                          options: option, 
                                          cancelToken: cancelToken
                                          );
        }
        
        ///json解析
        HttpResultBean<T> resultBean = _parseJsonToObject<T>(url, 
                                                             response, 
                                                             isResultList
                                                             );
        if (resultBean.isSuccess()) {
            ///请求成功
              _onRespSuccessCallback(
                  resultBean, 
                  onSuccessObjCallback, 
                  onSuccessListCallback
              );
        } else {
             ///请求失败
              _onRespErrorCallback(
                  isShowFailToast, 
                  onErrorCallback, 
                  resultBean.obtainErrorBean()
              );
        }
    } on DioError catch (e) {
        _onRespErrorCallback(
            isShowFailToast, 
            onErrorCallback, 
            _createErrorEntity(e)
        );
    } catch (exception) {
        LogUtils.e(_tag, ' 异常:${exception?.toString()}');
        _onRespErrorCallback(
            isShowFailToast,
            onErrorCallback,
            HttpErrorBean(
                code: HttpCode.fail, 
                message: exception?.toString() ?? '网络异常,请稍后再试'
            ),
        );
    } finally {
        ///请求完成,隐藏加载框
        if (isShowLoading) {
            _httpConfig.hideLoading(url, 
                    cancelToken.hashCode, 
                    cancelToken.isCancelled
            );
        }
    
        ///请求完成移除cancelToken
        if (cancelToken != null 
                && _cancelTokenList != null 
                && _cancelTokenList.contains(cancelToken)) {
            _cancelTokenList.remove(cancelToken);
        }
    
        ///请求完成回调
        onCompleteCallback?.call();
    }

}

///http响应json解析为对象
///[response] http 响应的对象
///[isRespListData] http响应的数据是否是List数据结构
HttpResultBean<T> _parseJsonToObject<T>(String url, Response response, 
                                        bool isRespListData) {
    if (response == null || response.data == null) {
        HttpResultBean<T> resultBean = HttpResultBean();
        resultBean.isRespListData = isRespListData;
        resultBean.code = HttpCode.unKnowError;
        resultBean.message = 'response is null';
        return resultBean;
    }
    
    //TODO 将json解析逻辑交给调用者处理
    HttpResultBean<T> resultBean = _httpConfig.parseJsonToObject<T>(
                             url, response.data, isRespListData);
    resultBean.json = response.data;
    return resultBean;
}

 ...省略其它代码
}

四、http拦截器使用

http请求时经常需要添加通用的请求参数和请求头,或是对响应数据进行预处理,通过添加拦截器来实现:

1、定义拦截器

///@date:  2021/2/25 14:22
///@author:  lixu
///@description: http拦截器,添加请求头和通用参数
class HeaderInterceptor extends InterceptorsWrapper {
  String _tag = 'HeaderInterceptor';

@override
onRequest(RequestOptions options) async {
    LogUtils.d(_tag, 'onRequest()');

    ///通用参数
    var params = {
        'lang': 'zhcn',
        'centerId': loginInfo.getCenterId(),
    };

    ///通过请求参数生成sign,添加到请求头
    String sign;
    if (XApi.methodGet == options.method) {
          options.queryParameters = 
                  (Map<String, dynamic>.from(
                      options.queryParameters ?? {}))..addAll(params);
          sign = await HttpUtils.getSignEncode(
                              options.queryParameters, 
                              HttpConst.serverKey
                       );
    } else {
          options.data = 
                  (Map<String, dynamic>.from(
                      options.data ?? {}))..addAll(params);
          sign = await HttpUtils.getSignEncode(
                              options.data, 
                              HttpConst.serverKey
                       );
    }

    ///添加请求头
    Map<String, String> headerMap = 
            loginInfo.token != null ? {'token': loginInfo.token} : {};
            
    headerMap.putIfAbsent('sign', () => sign);
    
    options.headers ??= {};
    options.headers.addAll(headerMap);

    return options;
}

@override
Future onResponse(Response response) async {
    ///从登录接口中获取token和用户信息
    ///TODO 也可以直接在登录响应的对象中获取用户信息和token,此处只是演示http拦截器功能
    if (response.request.path.contains(HttpUrls.loginUrl)) {
          HttpResultBean<LoginResultBean> resultBean = 
              HttpJsonUtils.parseJsonToObject(HttpUrls.loginUrl, 
                                              response.data, 
                                              false
                                              );
          if (resultBean.isSuccess()) {
                loginInfo.token = resultBean.data?.token;
                loginInfo.userBean = resultBean.data?.user;
          }
          LogUtils.i(_tag, '登录获取的token:${loginInfo.token}');
    }
    return response;
    
}
    
    
}

2、添加拦截器

///@date:  2021/2/26 14:01
///@author:  lixu
///@description: http相关配置
///当前配置在[XApi]类中被调用
class HttpConfigImpl implements IHttpConfig {
    String _tag = 'HttpConfigImpl';
    
    ...省略其它代码
    
    ///添加http拦截器
    ///拦截器队列的执行顺序是FIFO,先添加的拦截器先执行
    @override
    List<Interceptor> configInterceptors() {
        List<Interceptor> interceptors = [];
        interceptors.add(HeaderInterceptor());
        return interceptors;
    }

    ...省略其它代码
}

五、http请求获取单个对象

参考LoginViewModel中onLogin()方法:

///调用登录接口,获取单个对象
Future<bool> onLogin() async {
    Map<String, dynamic> map = {
        'account': '15015001500',
        'pass': '123qwe',
        'appType': 'PATIENT',
        'device': 'ANDROID',
        'push': '13065ffa4e22e63efd2',
    };

    ///http请求方法全部字段功能说明
    RequestOptions option = RequestOptions();
    option.method = XApi.methodPost;
    option.baseUrl = HttpUrls.httpHost;

    await XApi().request<LoginResultBean>(
            HttpUrls.loginUrl,
            params: map,
            //优先级最高
            method: XApi.methodPost,
            //针对当前请求的配置选项,优先级次高
            option: option,
            cancelToken: loginCancelToken,
            //请求前检测网络连接是否正常,如果连接异常,直接返回错误
            isCheckNetwork: true,
            //显示加载dialog
            isShowLoading: true,
            //加载dialog显示的提示文本
            loadingText: '正在登录...',
            //请求失败时显示toast提示
            isShowFailToast: true,
            //请求过程中可以关闭加载弹窗(请求过程中关闭dialog时自动取消请求)
            isCancelableDialog: true,
            onSuccess: (LoginResultBean bean) {
                _loginResultBean = bean;
                ToastUtils.show('登录成功');
            },
            onError: (HttpErrorBean errorBean) {
                _loginResultBean = null;
                LogUtils.e(getTag(), '登录失败');
            },
            onComplete: () {
                LogUtils.i(getTag(), '登录完成');
            },
    );
  return _loginResultBean?.token != null && _loginResultBean?.user != null;
}

六、http请求获取List对象

参考LoginViewModel中getUserList()方法

///获取用户列表List
Future<List<UserDetailBean>> getUserList() async {

    var params = {
        'userId': loginInfo.userBean?.userId,
        'token': loginInfo.token,
    };

    List<UserDetailBean> userList;
    await api.requestList<UserDetailBean>(HttpUrls.userListUrl,
                params: params,
                onSuccess: (List<UserDetailBean> list) {
                      userList = list;
                },
                onError: (HttpErrorBean errorBean) {
                      LogUtils.e(getTag(), '获取用户列表失败');
                },
    );

    return userList;
}

七、最简单的http请求

参考LoginViewModel中simplestHttpDemo()方法:

Future simplestHttpDemo(BuildContext context) async {
    var params = {
        'userId': loginInfo.userBean?.userId,
        'token': loginInfo.token,
    };
    
    ///最简单的http请求,可以满足大多数场景    
    await api.requestList<UserDetailBean>(HttpUrls.userListUrl,
                params: params,
                onSuccess: (List<UserDetailBean> list) {
                  ToastUtils.show('获取用户列表成功,用户数:${list?.length}');
                },
    );
}

上面最简单的http请求包含的功能:

1、自动显示、隐藏加载dialog,请求过程中dialog不能关闭 ;

2、请求失败自动显示Toast提示错误信息 ;

3、使用默认请求方法(IHttpConfig#configBaseOptions()对象配置的方法);

仅3行代码就能满足大多数http请求的场景,Demo地址

下一篇文章:Flutter MVVM实践