前言:封装只是加深自己的理解,网上已经有很优秀的封装,我也是借鉴了okgo和鸿洋的okhttputils。本项目是基于mvc模式下,但这篇只讲如何对okhttp进行封装(这里我按最基础步骤来,需要额外功能,看源码和本文理解,肯定可以实现)。
我们封装要有的功能有:
- 支持get请求
- 支持post请求
- 支持上传文件
- 支持下载文件和断点续传
- 有网络时,支持缓存(连接网络时的有效期)
- 断开网络,支持离线缓存(离线缓存有效期)
- 多次请求同一url,在网络还在请求时,是否只请求一次
- 支持请求失败,自动重连
先看看效果展示 (建议打开权限)
首先okhttp进行简单get请求代码是这样的:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
Request.Builder mBuilder = new Request.Builder();
mBuilder.url("url?parm1=x&parm2=y");
mBuilder.header("head","headValue");
Request okHttpRequest = mBuilder.build();
okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
get请求参数是拼在url后面的,而且上面是异步请求enqueue,这个方法是在子线程里的。回调onFailure和onResponse也都在子线程,我们可以把解析等耗时操作放在这里,但是这里不能直接更改UI,要把他回调到主线程里。
1、封装EasyOk
看到上面简单的网络请求,okHttpClient不可能一直new,而且在网络请求的时候要有取消网络请求,取消网络请求代码如下
//tag取消网络请求
public void cancleOkhttpTag(String tag) {
Dispatcher dispatcher = okHttpClient.dispatcher();
synchronized (dispatcher) {
//请求列表里的,取消网络请求
for (Call call : dispatcher.queuedCalls()) {
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
//正在请求网络的,取消网络请求
for (Call call : dispatcher.runningCalls()) {
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
}
}
可以看到,取消代码请求要用的okHttpClient,所以我们要保持okHttpClient的唯一性,这里就要用到单例了。所以EasyOk最开始是这样的:
public class EasyOk {
private static EasyOk okHttpUtils;
private OkHttpClient okHttpClient;
//这个handler的作用是把子线程切换主线程。在后面接口中的具体实现,就不需要用handler去回调了
private Handler mDelivery;
private EasyOk() {
mDelivery = new Handler(Looper.getMainLooper());
okHttpClient = new OkHttpClient.Builder()
.hostnameVerifier(new HostnameVerifier() {//证书信任
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
})
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
public static EasyOk getInstance() {
if (okHttpUtils == null) {
okHttpUtils = new EasyOk();
}
return okHttpUtils;
}
public OkHttpClient getOkHttpClient() {
return okHttpClient;
}
public Handler getmDelivery() {
return mDelivery;
}
//tag取消网络请求
public void cancleOkhttpTag(String tag) {
Dispatcher dispatcher = okHttpClient.dispatcher();
synchronized (dispatcher) {
//请求列表里的,取消网络请求
for (Call call : dispatcher.queuedCalls()) {
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
//正在请求网络的,取消网络请求
for (Call call : dispatcher.runningCalls()) {
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
}
}
}
mDelivery是将子线程切换到主线程的handler,这里借鉴了鸿洋大神的思路。因为我们最好把重复性的工作全部都放在封装里。封装最重要的优点不就是为了方便吗。
根据最原始的okhttp进行的get请求,我们还缺Request,还有一个网络请求的回调,接下来是:
2、封装OkGetBuilder(这里我将每种请求封装成了不同的Builder,虽然重复了好多操作,但更加清晰,偏于理解)
因为每次请求,Request 请求体都是需要new的,所以可想而知这里不可能是单例,而且每次调用请求都是new出来的Request。根据最原始的get请求,我们知道OkGetBuilder里需要1、url,2、参数,3、header,4、tag,5、还有自己的网络回调。
所以我们得用有个网络回调接口,这里我用的是抽象类ResultMyCall,这里用抽象类的好处是,我们可以把统一重复操作放在父类里,只要不重写方法,都会按父类方法去实现,所以这里有时候你只需要重写一个onSuccess方法即可,不像接口一样要把方法全部实现。要注意的是,如果要基于mvc,最好用接口ResulCall,这块到时候介绍mvc的时候回介绍。现在我们都按抽象类ResultMyCall走。抽象类如下ResultMyCall
public abstract class ResultMyCall<T> {
//请求网络之前,一般展示loading
public void onBefore() {
}
//请求网络结束,消失loading
public void onAfter() {
}
//监听上传图片的进度(目前支持图片上传,其他重写这个方法无效)
public void inProgress(float progress) {
}
//错误信息
public void onError(String errorMessage) {
ToastUtils.showToast(errorMessage);
}
public void onSuccess(Object response) {
}
//如果带了泛型T,这里个方法会获取泛型的type,用于解析,如果不带泛型,默认返回的是String
public Type getSuperclassTypeParameter(Class<?> subclass) {
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
return null;
}
ParameterizedType parameterized = (ParameterizedType) superclass;
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}
public Type getType() {
return getSuperclassTypeParameter(getClass());
}
}
相信这个类也很容易理解,这里的OnError我写了一个Toast。所以每次请求的回调onError可以不重写,会自动弹Toast,如果你需要有其他操作,比如说不弹Toast,是需要打开另外一个页面则可以重写这个方法如:
@Override
public void onError(String errorMessage) {
super.onError(errorMessage);
//注释super.onError(errorMessage);那么不会走父类方法,
}
那么简单OkGetBuilder如下:
public class OkGetBuilder {
private String url;
private String tag;
private Map<String, String> headers;
private Map<String, String> params;
private OkHttpClient okHttpClient;
private Context context;
private Handler mDelivery;
private Request okHttpRequest;
public OkGetBuilder() {
this.okHttpClient = EasyOk.getInstance().getOkHttpClient();
this.context = MyApplication.getContext();
this.mDelivery = EasyOk.getInstance().getmDelivery();
}
public OkGetBuilder build() {
Request.Builder mBuilder = new Request.Builder();
if (params != null) {
mBuilder.url(appendParams(url, params));
} else {
LogUtils.i("网络请求", "请求接口 ==>> " + url);
mBuilder.url(url);
}
if (!TextUtils.isEmpty(tag)) {
mBuilder.tag(tag);
}
if (headers != null) {
mBuilder.headers(appendHeaders(headers));
}
okHttpRequest = mBuilder.build();
return this;
}
public void enqueue(final ResultMyCall resultMyCall) {
if (resultMyCall != null) {
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onBefore();
}
});
}
okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
if (resultMyCall != null) {
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onAfter();
String errorMsg;
if (e instanceof SocketException) {
} else {
if (e instanceof ConnectException) {
errorMsg = context.getString(R.string.network_unknow);
} else if (e instanceof SocketTimeoutException) {
errorMsg = context.getString(R.string.network_overtime);
} else {
errorMsg = context.getString(R.string.server_error);
}
resultMyCall.onError(errorMsg);
}
}
});
}
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
//网络请求成功
if (response.isSuccessful()) {
if (resultMyCall != null) {
String result = response.body().string();
Object successObject = null;
try {
if (resultMyCall.getType() == null) {
successObject = result;
} else {
successObject = GsonUtil.deser(result, resultMyCall.getType());
}
} catch (Throwable e) {
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onAfter();
resultMyCall.onError("数据解析出错了");
}
});
return;
}
if (successObject == null) {
successObject = result;
}
final Object finalSuccessObject = successObject;
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onAfter();
resultMyCall.onSuccess(finalSuccessObject);
}
});
}
} else {
//接口请求确实成功了,code 不是 200
if (resultMyCall != null) {
final String errorMsg = response.body().string();
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onAfter();
resultMyCall.onError(errorMsg);
}
});
}
}
}
});
}
public OkGetBuilder url(String url) {
this.url = url;
return this;
}
public OkGetBuilder tag(String tag) {
this.tag = tag;
return this;
}
public OkGetBuilder headers(Map<String, String> headers) {
this.headers = headers;
return this;
}
public OkGetBuilder params(Map<String, String> params) {
this.params = params;
return this;
}
private Headers appendHeaders(Map<String, String> headers) {
Headers.Builder headerBuilder = new Headers.Builder();
if (headers == null || headers.isEmpty()) return null;
for (String key : headers.keySet()) {
headerBuilder.add(key, headers.get(key));
}
return headerBuilder.build();
}
//get 参数拼在url后面
private String appendParams(String url, Map<String, String> params) {
StringBuilder sb = new StringBuilder();
if (url.indexOf("?") == -1) {
sb.append(url + "?");
} else {
sb.append(url + "&");
}
if (params != null && !params.isEmpty()) {
for (String key : params.keySet()) {
sb.append(key).append("=").append(params.get(key)).append("&");
}
}
sb = sb.deleteCharAt(sb.length() - 1);
LogUtils.i("网络请求", "请求接口 ==>> " + sb.toString());
return sb.toString();
}
}
首先我们new这个类的时候把唯一的okHttpClient拿到用来进行请求,把mDelivery拿到,用于子线程切换主线程,那么在后面的回调方法里可以直接进行UI操作。OkGetBuilder里要做的是把heads用map传入,那么要进行一个循环add到head上去如:appendHeaders方法,参数也用map传入,拼接参数如:appendParams方法。
那么接下来就是请求这块:
1、在调用自定义方法enqueue(我这里和okhttp重名了偏于理解),首先我们这里就要回调onBefore,如:
resultMyCall.onBefore()
2、在原始回调失败onFailure里,当然是回调我们的onAfter和onError。
3、在原始回调onResponse里,这里会比较麻烦,即使返回code=200;这里还有2种情况,一种是正常的按你传入的泛型解析,一种是比如:点击关注,网络请求也成功了,但接口问题返回关注失败。我们公司用的是status为0代表失败,这里要特别注意
4、OkGetBuilder封装好了,把它放进EasyOk里如下:
public static OkGetBuilder get() {
return new OkGetBuilder();
}
3、最后封装后的进行GET请求用法如下:(其实post和上传文件都是这个思路,不同的是post有多种RequestBody,上传文件也是post的一种,这里具体可以看鸿洋和okgo的封装):
//这些是全部方法,没有用到的不使用
//paramsBuilder 是我封装用的一个传递参数的类。有要用的参数一致点下去就好了...
EasyOk.get().url("http://gank.io/api/xiandu/category/wow")
.tag("cancleTag")
//内部已经做了null处理,请求头部
//.headers(paramsBuilder.getHeads())
//内部已经做了null处理,请求参数
//.params(paramsBuilder.getParams())
.build().enqueue(new ResultMyCall<T>() {
@Override
public void onBefore() {
super.onBefore();
}
@Override
public void onAfter() {
super.onAfter();
}
@Override
public void onError(String errorMessage) {
super.onError(errorMessage);
}
@Override
public void onSuccess(Object response) {
super.onSuccess(response);
//如果你再new ResultMyCall的时候带了泛型,那么这里只需要
//T bean = (T)response ;
//如果没有带泛型,那么默认返回的string类型,
//Sring bean = (String)response;
}
});
如果onBefore和onAfter还有onError,都把统一操作封装好了并且不需要重写没有特殊操作的你可以这样:
EasyOk.get().url("http://gank.io/api/xiandu/category/wow")
.tag("cancleTag")
.build().enqueue(new ResultMyCall<T>() {
@Override
public void onSuccess(Object response) {
super.onSuccess(response);
//如果你再new ResultMyCall的时候带了泛型,那么这里只需要
//T bean = (T)response ;
//如果没有带泛型,那么默认返回的string类型,
//Sring bean = (String)response;
}
});
上面介绍我我把缓存和重连等其他功能没有说,因为加进去太复杂也不清晰,等后面单独拿出来讲,通过本文的理解,你会知道怎么去封装,加到什么地方去。
4、接下说的是下载文件及断点续传下载文件
经过上述介绍,咱们直接看OkDownloadBuilder里的内容:
public class OkDownloadBuilder {
//断点续传的长度
private long currentLength;
private String url;
private String tag;
//文件路径(不包括文件名)
private String path;
//文件名
private String fileName;
//是否开启断点续传
private boolean resume;
//只允许一个在当前下载线程中
private boolean onlyOneNet;
/**
* okHttpUtils里单例里唯一
*/
private OkHttpClient okHttpClient;
private Context context;
private Handler mDelivery;
/**
* 每次请求网络生成的请求request
*/
private Request.Builder mBuilder;
public OkDownloadBuilder() {
this.okHttpClient = EasyOk.getInstance().getOkHttpClient();
this.context = MyApplication.getContext();
this.mDelivery = EasyOk.getInstance().getmDelivery();
}
public OkDownloadBuilder build() {
mBuilder = new Request.Builder();
mBuilder.url(url);
if (!TextUtils.isEmpty(tag)) {
mBuilder.tag(tag);
}
//这里只要断点上传,总会走缓存。。所以强制网络下载
mBuilder.cacheControl(CacheControl.FORCE_NETWORK);
return this;
}
public void removeOnceTag() {
if (onlyOneNet) {
if (!TextUtils.isEmpty(tag)) {
EasyOk.getInstance().getOnesTag().remove(tag);
} else {
EasyOk.getInstance().getOnesTag().remove(url);
}
}
}
public void enqueue(final OnDownloadListener listener) {
if (onlyOneNet) {
if (!TextUtils.isEmpty(tag)) {
if (EasyOk.getInstance().getOnesTag().contains(tag)) {
return;
}
EasyOk.getInstance().getOnesTag().add(tag);
} else {
if (EasyOk.getInstance().getOnesTag().contains(url)) {
return;
}
EasyOk.getInstance().getOnesTag().add(url);
}
}
if (resume) {
File exFile = new File(path, fileName);
if (exFile.exists()) {
currentLength = exFile.length();
mBuilder.header("RANGE", "bytes=" + currentLength + "-");
}
}
Request okHttpRequest = mBuilder.build();
okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
removeOnceTag();
//下载失败监听回调
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownloadFailed(e);
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
removeOnceTag();
InputStream is = null;
byte[] buf = new byte[1024];
int len = 0;
FileOutputStream fos = null;
//储存下载文件的目录
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
final File file = new File(dir, fileName);
try {
is = response.body().byteStream();
//总长度
final long total;
//如果当前长度就等于要下载的长度,那么此文件就是下载好的文件
//前提是这里是默认下载的同意文件,要判断是否可以断点续传,最好在开启网络的时候判断是否是同意版本号
if (currentLength == response.body().contentLength()) {
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownloadSuccess(file);
}
});
return;
}
if (resume) {
total = response.body().contentLength() + currentLength;
} else {
total = response.body().contentLength();
}
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownLoadTotal(total);
}
});
if (resume) {
//这个方法是文件开始拼接
fos = new FileOutputStream(file, true);
} else {
//这个是不拼接,从头开始
fos = new FileOutputStream(file);
}
long sum;
if (resume) {
sum = currentLength;
} else {
sum = 0;
}
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
final int progress = (int) (sum * 1.0f / total * 100);
//下载中更新进度条
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownloading(progress);
}
});
}
fos.flush();
//下载完成
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownloadSuccess(file);
}
});
} catch (final Exception e) {
mDelivery.post(new Runnable() {
@Override
public void run() {
listener.onDownloadFailed(e);
}
});
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
}
}
}
});
}
public OkDownloadBuilder onlyOneNet(boolean onlyOneNet) {
this.onlyOneNet = onlyOneNet;
return this;
}
public OkDownloadBuilder path(String path) {
this.path = path;
return this;
}
public OkDownloadBuilder fileName(String fileName) {
this.fileName = fileName;
return this;
}
public OkDownloadBuilder url(String url) {
this.url = url;
return this;
}
public OkDownloadBuilder tag(String tag) {
this.tag = tag;
return this;
}
public OkDownloadBuilder resume(boolean resume) {
this.resume = resume;
return this;
}
如果你不看断点续传这块,在onResponse里其实就是输入流和文件流,把流写进文件里的操作;断点的续传的关键点是哪些?要知道断点续传其实就是接着上次未下载的文件继续下载(当然这里要确保下载的是同一文件,如下载更新要保证是同一版本号,这里在获得版本更新内容的时候判断是否是同一版本)。关键有2点,
1、在开启文件下载的时候,要在header里加上当前文件长度如:
mBuilder.header("RANGE", "bytes=" + currentLength + "-");
2、同时在文件流的时候要告诉流,我们不是覆盖,是拼接如:
fos = new FileOutputStream(file, true);//没错就是这个true
5、有网的时候的在线缓存
场景如下:假如首页广告,get请求下来的数据,而且这个可能1个星期才会换一次数据,这个时候如果没有这个功能,每次进首页都会去请求网络,如果有这功能,那么如果缓存内容在有效期就会跳过网络请求,直接取缓存。这样节约流量之余还能减轻服务器压力。当然要实现缓存,要设置缓存文件,在初始化okHttpClient时候设置缓存文件
//设置缓存文件路径,和文件大小
okHttpClent.cache(new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 50 * 1024 * 1024))
查阅大量的资料,大量博客都很坑。坑的你懵逼这里给大家看下正解:okhttp缓存正解。
看到这篇的时候,你就知道其他地方都可以不用管,用拦截器可以实现,但是要清楚什么是在线缓存,什么是离线缓存。这是2个概念。在okHttpClient加上网络拦截器如下:
okHttpClient.addNetworkInterceptor(NetCacheInterceptor.getInstance())
这里用的拦截器,我使用了单例,这样便于通过改变参数,可以达到是否使用缓存;NetCacheIntertor代码如下:
/**
* Created by leo
* on 2019/7/25.
* 在有网络的情况下
* 如果还在网络有效期呢则取缓存,否则请求网络
* 重点 : 一般okhttp只缓存不大改变的数据适合get。(个人理解 : 例如你设置了我的方案列表接口的缓存后,你删除了一条方案,刷新下。
* 他取的是缓存,结果那条删除的数据会出来。这个时候这个接口,不适合用缓存了)
* (这里注意,如果一个接口设置了缓存30秒,下次请求这个接口的30秒内都会去取缓存,即使你设置0也不起效。因为缓存文件里的标识里已经有30秒的有效期)
*/
public class NetCacheInterceptor implements Interceptor {
private static NetCacheInterceptor cacheInterceptor;
//30在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
private int onlineCacheTime;
public static NetCacheInterceptor getInstance() {
if (cacheInterceptor == null) {
cacheInterceptor = new NetCacheInterceptor();
}
return cacheInterceptor;
}
private NetCacheInterceptor() {
}
public void setOnlineTime(int time) {
this.onlineCacheTime = time;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder1 = request.newBuilder();
//这里我们登陆,在head里获取令牌token存起来,网络请求的时候把令牌加入head,用于身份区分
String token = (String) PreferenceUtil.get("USER_TOKEN", "");
if (!TextUtils.isEmpty(token)) {
builder1.addHeader("Token", token)
.build();
}
request = builder1.build();
Response response = chain.proceed(request);
List<String> list = response.headers().values("Token");
if (list.size() > 0) {
PreferenceUtil.put("USER_TOKEN", list.get(0));
}
//这里是设置缓存的操作
if (onlineCacheTime != 0) {
//如果有时间就设置缓存
int temp = onlineCacheTime;
Response response1 = response.newBuilder()
.header("Cache-Control", "public, max-age=" + temp)
.removeHeader("Pragma")
.build();
onlineCacheTime = 0;
return response1;
} else {
//如果没有时间就不缓存
Response response1 = response.newBuilder()
.header("Cache-Control", "no-cache")
.removeHeader("Pragma")
.build();
return response1;
}
// return response;
}
}
max-age是在线缓存的有效时间,如果我设置了max-age = 3600,那么意思是在首次请求网络缓存下来的数据后,在1小时之内都将会直接取缓存,跳过网络请求。这里我获取token是通过拦截器获取的,response.headers()可以获得,服务器返回的所有head信息,包括set-cookie信息。而且okhttp提供了.cookjar()。可以通过cookie持久化等自定义。
这些设置完怎么验证呢?回到你原始网络请求onResponse回调里;通过:
if (response.networkResponse()!=null){
LogUtils.i("内容来源","来自网络请求");
}
if (response.cacheResponse()!=null){
LogUtils.i("内容来源","来自缓存");
}
当然这里只是验证,在onResponse不需要改变,okhttp内部已经做好了所有的工作。
6、无网络的时候的离线缓存
例如腾讯新闻等,在你手机开启飞行模式的时候,在进app的时候,还是会依旧显示之前加载的数据。这个是就是离线缓存,离线缓存和在线缓存最大的区别,在线缓存即使有条件请求网络也可以跳过网络取缓存。同样通过拦截器添加,在无网络的时候,是不会走addNetworkInterceptor方法的。但是通过addInterceptor,有没有网都会走,而且addInterceptor会先于addNetworkInterceptor运行
okHttpClient.addInterceptor(OfflineCacheInterceptor.getInstance())
同样我也用了单例,具体如下:
/**
* Created by leo
* on 2019/7/25.
* 这个会比网络拦截器先 运行
* 在没有网络连接的时候,会取的缓存
* 重点 : 一般okhttp只缓存不大改变的数据适合get。(个人理解,无网络的时候可以将无网络有效期改长点)
* 这里和前面的不同,立即设置,立即生效。例,你一个接口设置1个小时的离线缓存有效期,立即设置0.下次进入后,则无效
*/
public class OfflineCacheInterceptor implements Interceptor {
private static OfflineCacheInterceptor offlineCacheInterceptor;
//离线的时候的缓存的过期时间
private int offlineCacheTime;
private OfflineCacheInterceptor() {
}
public static OfflineCacheInterceptor getInstance() {
if (offlineCacheInterceptor == null) {
offlineCacheInterceptor = new OfflineCacheInterceptor();
}
return offlineCacheInterceptor;
}
public void setOfflineCacheTime(int time) {
this.offlineCacheTime = time;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!NetWorkUtils.isNetworkConnected(MyApplication.getContext())) {
if (offlineCacheTime != 0) {
int temp = offlineCacheTime;
request = request.newBuilder()
// .cacheControl(new CacheControl
// .Builder()
// .maxStale(60,TimeUnit.SECONDS)
// .onlyIfCached()
// .build()
// ) 两种方式结果是一样的,写法不同
.header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
.build();
offlineCacheTime = 0;
} else {
request = request.newBuilder()
.header("Cache-Control", "no-cache")
.build();
}
}
return chain.proceed(request);
}
}
NetWorkUtils是一个判断有没有网络的工具类。你可以看到这里是max-stale,这里我理解就是要设置的离线缓存有效期,如果设置为max-stale=3600就是离线缓存1个小时。如果你去深山老林,在长达1小时01分的时候,离线缓存失效,那么你在此进app,页面将空白。当然你也可以设置离线缓存一直有效,Integer.MAX_VALUE。
7、多次请求同一url,在网络请求未结束,是否只请求一次。
这里当然你可以利用tag来做,如果当前正在请求的池里的call.request.tag()或等待请求队列里的tag,包含你当前请求网络tag时,则不请求网络,只显示loading。但是这样的话必须每次都要加上tag。所以我直接在EasyOk里加上了一个
//防止网络重复请求的tagList;
private ArrayList<String> onesTag;
如果没有tag的时候,这里我可以用请求url。在网络请求成功和结束的时候我从这个集合里remove掉这个元素。当然你会说,当我们取消网络请求的时候呢,其实取消网络请求的时候会走onFailure(Call call,IOException e)。这个时候错误类型是SocketException。所以你取消网络的时候是会走onFailure,同样会remove掉、代码大致如下(只放相关代码):
public void enqueue(final ResultCall resultMyCall) {
if (resultMyCall != null) {
//这里是子线程切换到主线程的操作,只要请求网络,我们都调onBefore,这里就是展示loading
mDelivery.post(new Runnable() {
@Override
public void run() {
resultMyCall.onBefore();
}
});
}
//这里是否带了onlyOneNet参数,默认是不开启的,return后将不继续往下走,就不会开启网络了
if (onlyOneNet) {
if (!TextUtils.isEmpty(tag)) {
if (EasyOk.getInstance().getOnesTag().contains(tag)) {
return;
}
EasyOk.getInstance().getOnesTag().add(tag);
} else {
if (EasyOk.getInstance().getOnesTag().contains(url)) {
return;
}
EasyOk.getInstance().getOnesTag().add(url);
}
}
...
}
8、请求失败自动重连,以及重连次数
这里也查阅了大量博客,都说okhttp有设置重连,设置okHttpClient.retryOnConnectionFailure(true)既可用重连,但是我大量测试发现,然并软(有明白的小伙伴,求告知)。这里我用了自己的方式,只要走onFailure,那么我们看看有没有设置重连和重连次数,代码如下,此代码都在builder下,每次网络请求都会new一个builder,以OkGetBuilder为例:
okHttpClient.newCall(okHttpRequest).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
//这里是取消网络请求,那么不用重连了
if (e instanceof SocketException) {
} else {
//tryAgainCount是重连,不设置,默认是0.不开启重连功能
//currentAgainCount,是OkGetBuilder里的属性,每次开启网络都会new一个Builder
//当然currentAgainCount初始是0
if (currentAgainCount < tryAgainCount && tryAgainCount > 0) {
currentAgainCount++;
//这里就是重连操作,call.request会获取你最开始的request
//this这里就是你当前new Callback,所以网络回调还会走这里
okHttpClient.newCall(call.request()).enqueue(this);
return;
}
}
}
至此,大致介绍完了。以上介绍的到底怎么用可以去我的github看看以上的用法
9、那么基于mvc封装怎么用,下面我们具体来说说这块
我定义了NetWorListener(get,post,上传文件网络回调),OnDownloadListener(文件下载网络回调),PermissionListener(权限申请结果回调),如果你想请求网络,只要实现这个接口,然后通过ModelSuperImpl去调用网络请求,就能在任何你实现这个接口的页面拿到网络请求回调,请求只需要这样:
调用网络: //在外面调用只需要传入参数,把url和解析类什么的都放在ModelSuperImpl里 ModelSuperImpl.netWork().gankGet(ParamsBuilder.build().params(PARAMS.gank("android")) .command(GANK_COMMAND), this);
ModelSuperImpl大致如下:
public class ModelSuperImpl extends ModelBase { private static final ModelSuperImpl ourInstance = new ModelSuperImpl(); public static ModelSuperImpl netWork() { return ourInstance; } public static ModelPermissionImpl permission() { return new ModelPermissionImpl(); } private ModelSuperImpl() { } public void gankGet(ParamsBuilder paramsBuilder, NetWorkListener netWorkListener) { paramsBuilder.url(SystemConst.GANK_GET) .type(new TypeToken<ResponModel<User>>() { }.getType()) ; sendOkHttpGet(paramsBuilder, netWorkListener); }
}
在Activity/Fragment里或是只要实现接口的地方拿到回调就是这样,command是为了区分一个页面可能请求多个网络请求:
@Override public void onNetCallBack(int command, Object object) { switch (command) { case GANK_COMMAND: Response<User> userModel = (Response<User>)object; break; } }
这里的ParamsBuilder有多个参数具体如下:
/**
* Created by leo
* on 2019/7/11.
*/
public class ParamsBuilder {
//请求网络的url(必填)
private String url;
//网络回调的int值(必填)
private int command;
//网络返回的type类型(选填)不填,则会返回string类型
private Type type;
//网络请求需要带的头部信息(选填,不填为null)
private HashMap<String, String> heads;
//网络请求需要带的参数(选填,不填为null)
private HashMap<String, String> params;
//网络loading需要带的文字信息(选填,不填为null)
private String loadMessage;
//是否显示网络loading(默认为显示loading)
private boolean isShowDialog = true;
//网络请求的tag,可根据tag取消网络请求(选填,不填:默认当前宿主类名,退出后自动取消)
private String tag;
//是否重写网络问题还是超时问题对回调进行一个重写
//如果是true,则在回调的时候可对那部分额外操作,除了弹提示还可以做别的操作
//(选填,不填:重写不了且只弹提示)
private boolean overrideError;
//json上传要带的参数
private String json;
//网络接口code=200, 但没有成功,此用户已关注
//需要重写带true,重写可以写逻辑包括弹提示
//不需要重写只弹提示
private boolean successErrorOverrid;
//离线缓存时间 单位秒
private int cacheOfflineTime;
//有网络请求时缓存最大时间
private int cacheOnlineTime;
//多次点击按钮,只进行一次联网请求
//场景:网络还在loading,又点了一次请求,那么不发送新请求,只显示loading
private boolean onlyOneNet = true;
//联网失败,重试次数
private int tryAgainCount;
//如果是在网络请求接口回调不是activity,也不是fragment,用于传context
//用于showdialog,当请求网络的页面不是Activity或是Fragment时必传
private Context context;
/**
* 下载文件才用的到
*/
private String path;
private String fileName;
//是否开启断点续传,要注意的是开启断点续传,要保证下载的是同一文件
//默认是不开启断点续传,除非判断要下载文件和当前未下载文件属于同一文件
//如果不是那么重新下载,会清掉之前的文件。
private boolean resume;
}
看到上面,具体设计的参数都写上了。但是我没有封装的很完美,当然必传的字段,没传时,你可以throw new NullPonintException("参数没传");把异常抛出去,一旦不传,程序就崩溃了。同样在下载文件只需要实现OnDownliadListener,即可。调用只需要这样:
ModelSuperImpl.netWork().downApk(ParamsBuilder.build().path(path) .fileName(fileName).tag("downApk"), this);
这里我把请求权限所有逻辑封装在了 ModelPerissionImpl里,只要实现PerimissionListener即可拿到网络请求回调,调用只需这样:
//RESUME_COMMAND一个页面可能要请求多个权限,用于区分,this即是PerimissionListener实现类,后面权限参数是可变的如果有多个权限可以一直逗号加下去 ModelSuperImpl.permission().requestPermission(RESUME_COMMAND, this, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE); //回调只需要这样;command是区分一个页面多个权限申请,如果一个页面只有1个申请那么可以不传commad @Override public void permissionSuccess(int command) { switch (command) { case NORMAL_COMMAND: ModelSuperImpl.netWork().downApk(ParamsBuilder.build().path(path) .fileName(fileName).tag("downApk"), this); break; } }
大致思路:我把具体的联网操作和具体的解析封装在了抽象类ModelBase,把具体的权限申请逻辑封装在了ModelPerissionImpl,ModelSuperImpl只负责调用网络请求方法,和权限申请方法,这样代码完美分隔了代码。视图层View收到了用户的操作或点击请求,响应controller,controller通知model处理逻辑业务,拿到结果通过接口告诉controller去更新Ui。加入你多个页面需要请求同一个url,你只需要通过ModelSuperImpl去调用方法就Ok了。
这里肯定很多人对ParamBuilder里的overrideError和successErrorOverrid不是很理解。其实这里默认不是重写方法,例如网络请求失败,我在ModelBase默认是弹出toast,如果是code=200,但是有可能接口走的错误方法,如关注失败,我也是默认不重写弹出toast。有可能实际操作需要我们做别的,如网络请求错误,需要我们跳另外一个页面,那么这个时候就需要你带.overrideError(true)和.successErrorOverrid(true)代码如下:
@Override
public void onNetCallBack(int command, Object object) {
switch (command) {
case GANK_COMMAND:
/**
* 请求接口失败(网络错误,或超时等造成)
* 如果需要重写则请求接口的时候加上.overrideError(true)
* 那么在下面代码写上逻辑,如果不需要重写,已经封住了会自动弹出错误提示,而且重
写会无效
* */
// if (obj instanceof NetFail) {
// NetFail netFailBean = (NetFail) obj;
// 处理逻辑
// return;
// }
/**
* 这是接口请求成功
* 如果请求接口,写了.successErrorOverrid(true)
* 说明重写了虽然code=200,返回的result 不是1;但是需要做其他逻辑不只是Toast才需要true
* 如果只是需要Toast错误信息,那么可以不写,下面的就不用重写了。封装已经默认弹提示
* */
// if (obj instanceof ErrorBean) {
// ErrorBean errorBean = (ErrorBean) obj;
// 处理逻辑
// return;
// }
ResponModel<User> detailModel = (ResponModel<User>) obj;
//更新UI
break;
}
}
结束语:我个人思路封装。如有不对欢迎指正,且如果有更好的思路欢迎留言。技术界的小学生,喜欢学习。看到这里,如果有帮助到你,帮博主star下吧
github地址
记得把builder里的EventBus全部删除,我之前没考虑太多,所以在展示效果的时候用EventBus传递了p.p
当然这里用的泛型类ResponModel,ErrorBean,NetFail都是我根据我的项目来定义的,记得如有数据结构不同请修改成需要的