网络选择
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,
);
}
- 文件结果如下
log
dio_logger
是为Dio定制的,只要一行代码就可以了dio.interceptors.add(dioLoggerInterceptor);
log内容按照request和response分开,基本能用。
颜色区分没有,不知道什么原因。
以拦截器的形式作为Dio的配件引入,感觉还是不错的。