写在前面
之前基于OkHttp3封装了一个网络请求框架,本篇接着上一篇的内容继续对Retrofit作一个封装,所以首先基本的Retrofit的用法你要清楚,起码了解过它发起请求的一个完整过程。最近这些天我对这个网络请求库进行了简单的测试,因为处理的业务并不多,所以暂时还没有出现什么太大的问题,这篇文章在草稿箱里封存了这么久了,今天是2月12号了,年前事年前毕,明天就回家了,今天把它放出来了,如果大家发现了什么问题,欢迎给我留言。
一、项目地址
GitHub地址:https://github.com/square/retrofit
官网地址:http://square.github.io/retrofit/
个人封装Retrofit案例地址:https://github.com/JArchie/NetTest
二、封装思路讲解(这里是摘取核心代码说明封装思路,完整代码参见上面给出的案例地址)
(一)基本请求的实现
1、创建项目
首先我们新建一个工程,然后在该工程下New Module,选择Android Library,然后新建一个包名称为retrofit,我们所有封装的网络请求相关的类都放在这个包下面,主要用来进行RESTful请求的(一种软件的架构,推荐阮一峰大神的这篇文章:http://www.ruanyifeng.com/blog/2011/09/restful.html)。在具体代码实现之前,我们还必须要做的一步操作是,把retrofit相关的依赖都添加到build.gradle文件中,大家可以去项目主页上按步骤添加,这里我就直接从我的项目中复制了:
//网络库
compile 'com.squareup.okhttp3:okhttp:3.9.1'
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.squareup.retrofit2:converter-scalars:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'
为了使封装更加灵活,基于传入参数,并且无顺序要求的原则,建造者模式无疑是最佳的选择。
2、创建Service接口
首先Retrofit的使用必须要有一个接口,这里定义为RestService,里面定义一系列的方法,用来发起具体的请求,为了更好的理解我下面代码的含义,这里带大家先来了解一下几个注解的具体含义:
Http请求方法注解:
标记类注解:
参数类注解:
鉴于是通用的封装,所以这里不会传入具体的路由信息,这里参数均使用Map以键值对的形式定义,因为value不确定具体的类型,所以我们类型给它Object,然后每个方法的返回类型都是Retrofit2中的Call类型,如果你要使用RxJava,这里需要返回Observable类型,之后我们需要操作这个Call对象,来看本类的代码:
public interface RestService {
//Get请求
@GET
Call get(@Url String url, @QueryMap Map params);
//Post请求
@FormUrlEncoded
@POST
Call post(@Url String url, @FieldMap Map params);
//Post原始数据
@POST
Call postRaw(@Url String url, @Body RequestBody body);
//Put请求
@FormUrlEncoded
@PUT
Call put(@Url String url, @FieldMap Map params);
//Put原始数据
@PUT
Call putRaw(@Url String url, @Body RequestBody body);
//Delete请求
@DELETE
Call delete(@Url String url, @QueryMap Map params);
//文件下载
@Streaming
@GET
Call download(@Url String url, @QueryMap Map params);
//文件上传
@Multipart
@POST
Call upload(@Url String url, @Part MultipartBody.Part file);
}
3、创建请求方法类型管理类
新建一个类HttpMethod,这个类用来统一管理请求方法的类型,这个类其实是可有可无的,但是本着类多代码少的原则,类的数量多,类中代码少,这样整体的架构将会更加清晰,代码看着也更加整洁,这也是面向对象六大原则中单一职责原则(SRP)所推荐的(注意:这个尽量不要使用枚举类型,因为使用枚举内存会飙升,本人一开始就是定义的枚举类,后来Review代码时又改成常量类了),具体代码体现如下:
/**
* Created by Jarchie on 2017\12\7.
* 统一管理请求方法类型
*/
public final class HttpMethod {
public static final String GET = "GET";
public static final String POST = "POST";
public static final String POST_RAW = "POST_RAW";
public static final String PUT = "PUT";
public static final String PUT_RAW = "PUT_RAW";
public static final String DELETE = "DELETE";
public static final String UPLOAD = "UPLOAD";
}
4、创建Retrofit的构建类
这里新建一个类RestCreator,这里面会使用到Java并发编程中推荐的单例模式的创建方式——内部类Holder,这里创建了一个静态内部类RetrofitHolder,用来构建全局的Retrofit对象,这里配置了Retrofit的baseUrl、client、并且添加了它的转换器,在配置client属性时,因为它需要一个OkHttpClient,所以我们这里又创建了一个静态内部类OkHttpHolder用来构建OkHttpClient对象,然后传入Retrofit的client属性中,通过写代码我们可以发现,Retrofit和OkHttp本身在设计的时候也是大量的使用了建造者模式,可见这种设计模式还是很受欢迎的,但是有一点比较烦,就是写起来比较累,很繁琐,后面在代码中会有详细的体现。又扯远了啊,继续说这个类,我们还在这个类中创建了OKHTTP的日志拦截器,这样我们在请求有响应时可以通过日志信息清楚的看到返回的数据,具体代码如下所示:
public final class RestCreator {
private static final class RetrofitHolder {
private static final String BASE_URL = Constant.BASE_URL;
private static final Retrofit RETROFIT_CLIENT = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(OKHttpHolder.OK_HTTP_CLIENT)
.addConverterFactory(ScalarsConverterFactory.create())
.build();
}
private static final class OKHttpHolder {
private static final int TIME_OUT = 20;
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.addInterceptor(getLoggerInterceptor())
.build();
}
//创建OKHTTP的日志拦截器
private static HttpLoggingInterceptor getLoggerInterceptor() {
//日志显示级别
HttpLoggingInterceptor.Level level = HttpLoggingInterceptor.Level.BODY;
//新建log拦截器
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(
new HttpLoggingInterceptor.Logger() {
@Override
public void log(@NonNull String message) {
Log.e("ResponseBody------->", message);
}
});
loggingInterceptor.setLevel(level);
return loggingInterceptor;
}
}
5、请求回调的处理
这里我们新建一个包callback,在这个包下我们新建四个接口IRequest、ISuccess、IFailure、IError,相对应的分别用来处理请求开始结束、请求成功、请求失败、请求错误等几种状态下的回调。具体代码如下:
public interface IRequest {
//请求开始
void onRequestStart();
//请求结束
void onRequestEnd();
}
public interface ISuccess {
//请求成功
void onSuccess(Object response);
}
public interface IFailure {
//请求失败
void onFailure();
}
public interface IError {
//请求错误
void onError(int code, String msg);
}
接着我们创建一个RequestCallbacks类,该类实现Retrofit2包下的Callback接口,这里注意别导错包了,这个类我们用来具体处理网络请求的回调过程,这个类中我创建了一个class字节码变量,在返回成功时使用Gson解析,将解析返回的结果就是一个数据实体对象返回到应用层中,在ISuccess接口中我们的参数类型定义为了Object,所以可以很灵活的强转成你自己的实体类型。如果你不想在框架层就解析成实体,比如有些业务情况返回的JSON数据比较简单,这里也做了一层处理,如果有实体对象就解析,没有就直接将原生JSON返回到应用层中去,做到了灵活处理。
public class RequestCallbacks implements Callback {
private final IRequest REQUEST;
private final ISuccess SUCCESS;
private final IFailure FAILURE;
private final IError ERROR;
private final Class> CLASS;
public RequestCallbacks(IRequest request, ISuccess success, IFailure failure, IError error, Class> clazz) {
this.REQUEST = request;
this.SUCCESS = success;
this.FAILURE = failure;
this.ERROR = error;
this.CLASS = clazz;
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
if (call.isExecuted()) {
if (SUCCESS != null) {
if (CLASS != null) {
Object object = new Gson().fromJson(response.body(), CLASS);
SUCCESS.onSuccess(object);
} else {
SUCCESS.onSuccess(response.body());
}
}
}
} else {
if (ERROR != null) {
ERROR.onError(response.code(), response.message());
}
}
DialogLoader.dismiss();
}
@Override
public void onFailure(@NonNull Call call, @NonNull Throwable t) {
if (FAILURE != null) {
FAILURE.onFailure();
}
if (REQUEST != null) {
REQUEST.onRequestEnd();
}
DialogLoader.dismiss();
}
}
6、开启建造者模式
准备工作结束之后,我们来开始建造者的创建。这里首先创建一个宿主类RestClient,然后再创建一个建造者类RestClientBuilder,好了文件创建完成了先放着不要急着写,我们先来思考一下网络请求一般会有哪些参数?我们很容易想到的参数如下:URL、传入的值(请求参数)、回调、请求体(RequestBody这是OkHttp3中的内容)这些东西,别忘了定义我们上面准备好的字节码变量,用来控制数据解析的哦。我们的RestClient在每次Builder去build的时候,它都会生成一个全新的实例,这里面的参数是一次构建完毕,绝不允许更改的,所以我们在声明的时候使用final关键字来声明,这样能保证每一次传值的原子性,在多线程中也是一种比较安全的做法。用final声明的变量如果没有赋值的话,必须在构造方法中为其赋值,来看代码:
private final Context CONTEXT;
private final String URL;
private final HashMap PARAMS;
private final RequestBody BODY;
private final IRequest REQUEST;
private final ISuccess SUCCESS;
private final IFailure FAILURE;
private final IError ERROR;
private final Class> CLASS;
public RestClient(Context context, String url, HashMap params,
RequestBody body, IRequest request,
ISuccess success, IFailure failure, IError error,
Class> clazz) {
this.CONTEXT = context;
this.URL = url;
this.PARAMS = params;
this.BODY = body;
this.REQUEST = request;
this.SUCCESS = success;
this.FAILURE = failure;
this.ERROR = error;
this.CLASS = clazz;
}
这样我们就可以创建我们的建造者了,代码如下:
public static RestClientBuilder Builder() {
return new RestClientBuilder();
}
我们接着来看一下RestClientBuilder这个类里面有哪些方法?Builder里面就是一些传值的操作,所以我们需要把宿主类里面的参数都照搬过来,当然了这里不能再使用final关键字修饰了,否则我们不能为其依次赋值了,所以我们就按照普通的方式来声明了:
private Context mContext = null;
private String mUrl = null;
private HashMap PARAMS = new HashMap<>();
private RequestBody mBody = null;
private IRequest mIRequest = null;
private ISuccess mISuccess = null;
private IFailure mIFailure = null;
private IError mIError = null;
private Class> mClass = null;
这里我们不允许外部的类去直接new它,只允许同包的RestClient去new它,所以这里在构造方法中做一个限制:
RestClientBuilder() {}
接下来是一系列的具体构建上面声明的这些参数的方法,这里我们同样使用了final关键字修饰这些方法,不允许外部修改它,构建这些参数的代码其实都是很类似的,写了一个其它的直接复制粘贴,修修改改就OK了,来看具体代码:
public final RestClientBuilder context(Context context) {
this.mContext = context;
return this;
}
public final RestClientBuilder url(String url) {
this.mUrl = url;
return this;
}
public final RestClientBuilder params(HashMap params) {
PARAMS.putAll(params);
return this;
}
public final RestClientBuilder params(String key, Object value) {
PARAMS.put(key, value);
return this;
}
public final RestClientBuilder raw(String raw) {
this.mBody = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), raw);
return this;
}
public final RestClientBuilder onRequest(IRequest iRequest) {
this.mIRequest = iRequest;
return this;
}
public final RestClientBuilder listener(ISuccess iSuccess) {
this.mISuccess = iSuccess;
return this;
}
public final RestClientBuilder listener(IFailure iFailure) {
this.mIFailure = iFailure;
return this;
}
public final RestClientBuilder listener(IError iError) {
this.mIError = iError;
return this;
}
public final RestClientBuilder clazz(Class> clazz) {
this.mClass = clazz;
return this;
}
好了,接着我们来build我们的RestClient,其实就是宿主的RestClient通过它自己的建造者RestClientBuilder去返回它本身的对象:
public final RestClient build() {
return new RestClient(mContext, mUrl, PARAMS, mBody, mIRequest,
mISuccess, mIFailure, mIError, mClass);
}
7、发起网络请求
构建了RestClient对象之后,我们还没有调起网络请求的方法呢,不然写了这么多也是毫无卵用。
首先我们要拿到RestService接口的对象,这里为了方便,在RestCreator这个类中使用内部类Holder的模式去实现,然后对外提供一个返回RestService类对象的静态方法去获取:
public static RestService getRestService() {
return RestServiceHolder.REST_SERVICE;
}
private static final class RestServiceHolder {
private static final RestService REST_SERVICE =
RetrofitHolder.RETROFIT_CLIENT.create(RestService.class);
}
拿到RestService之后,我们来接着写我们的实现请求的方法request(String method)方法,参数是区分各个请求类型的字符串,之前我们已经在HttpMethod类中定义好了,不知道大家还有印象吗?在这个方法中我们通过RestService类对象去调用内部定义好的通用的请求方法(get、post、put、Delete等等),然后返回一个Call对象,注意也是Retrofit2包下面的。调用完了具体的方法之后,我们最后去处理retrofit的回调,就是我们上面定义好的RequestCallbacks类中的内容,代码如下:
private void request(String method) {
final RestService service = RestCreator.getRestService();
Call call = null;
if (REQUEST != null) {
REQUEST.onRequestStart();
}
//弹出加载框
DialogLoader.show(CONTEXT);
switch (method) { //调起Service中相对应的请求类型
case HttpMethod.GET: //GET请求
call = service.get(URL, PARAMS);
break;
case HttpMethod.POST: //POST请求
call = service.post(URL, PARAMS);
break;
case HttpMethod.POST_RAW: //POST原始数据
call = service.postRaw(URL, BODY);
break;
case HttpMethod.PUT: //PUT请求
call = service.put(URL, PARAMS);
break;
case HttpMethod.PUT_RAW: //PUT原始数据
call = service.putRaw(URL, BODY);
break;
case HttpMethod.DELETE: //DELETE请求
call = service.delete(URL, PARAMS);
break;
case HttpMethod.UPLOAD: //上传文件
final RequestBody requestBody =
RequestBody.create(MediaType.parse(MultipartBody.FORM.toString()), FILE);
final MultipartBody.Part body =
MultipartBody.Part.createFormData("file", FILE.getName(), requestBody);
call = service.upload(URL, body);
break;
default:
break;
}
if (call != null) {
call.enqueue(getRequestCallback());
}
}
//获取处理回调的方法
private Callback getRequestCallback() {
return new RequestCallbacks(REQUEST, SUCCESS, FAILURE, ERROR, CLASS);
}
最后我们再定义如下几个方法:get()、post()、put()、delete()、upload(),方法内部去调用request()方法,在建造者构建完成时调用,用来真正发起相对应的请求:
//GET请求
public final void get() {
request(HttpMethod.GET);
}
//POST请求
public final void post() {
if (BODY == null) {
request(HttpMethod.POST);
} else {
if (!PARAMS.isEmpty()) {
throw new RuntimeException("params must be null!");
}
request(HttpMethod.POST_RAW);
}
}
//PUT请求
public final void put() {
if (BODY == null) {
request(HttpMethod.PUT);
} else {
if (!PARAMS.isEmpty()) {
throw new RuntimeException("params must be null!");
}
request(HttpMethod.PUT_RAW);
}
}
//DELETE请求
public final void delete() {
request(HttpMethod.DELETE);
}
//上传文件
public final void upload() {
request(HttpMethod.UPLOAD);
}
8、调用说明
在做完了以上这些工作之后,其实我们就已经把我们的RestClient的雏形构建出来了,链式调用结构清晰,让人看着神清气爽啊,发起请求的代码就写成了这个样子了:
RestClient.Builder()
.context(this)
.clazz(null)
.params("","")
.listener(new ISuccess() {
@Override
public void onSuccess(Object response) {
}
})
.listener(new IFailure() {
@Override
public void onFailure() {
}
})
.listener(new IError() {
@Override
public void onError(int code, String msg) {
}
})
.build()
.post();
三、文件下载
文件下载其实就是个GET请求,这里需要注意大文件下载时一定要添加@Streaming注解,这是官方提到的一点,因为文件下载时是一次性读到内存中的,不加这个很容易造成内存溢出。下面我们就来具体的实现一下文件下载,其实过程都差不多。
1、处理耗时任务
因为文件下载是一个耗时的过程,所以我们不能直接在主线程中进行,需要在子线程中处理,然后将结果转发到主线程中。这里我们新建一个类SaveFileTask继承自AsyncTask类,第一个输入参数类型我们传入Object,第二个处理过程参数类型传入Void,第三个输出参数类型我们传入File类型,在doInBackground方法中,首先是拿到文件目录、后缀名、文件名、输入流这些内容,然后通过IO流进行文件写入的操作,在onPostExecute方法中,处理执行完的结果,此时是被主线程调用的,简单起见只处理成功的回调,通过SUCCESS.onSuccess()方法,参数传入文件路径,将下载的结果进行返回,如下所示:
public final class SaveFileTask extends AsyncTask
2、处理下载逻辑及回调
新建一个类DownloadHandler,然后定义好网络请求的URL、参数、文件目录、后缀名、文件名、相关回调接口,最后通过RestService调用内部定义的download方法,实现Callback,泛型传入ResponseBody,在重写的成功和失败的回调方法里进行具体的实现,成功的回调中通过我们上面定义的SaveFileTask类的对象去调用下载的执行方法进行真正的下载,这里调用的是executeOnExecutor方法,它通常和THREAD_POOL_EXECUTOR一起使用,允许多个任务在由AsyncTask管理的线程池中并行执行,代码如下:
public class DownloadHandler {
private final String URL;
private final HashMap PARAMS;
private final IRequest REQUEST;
private final String DOWNLOAD_DIR;
private final String EXTENSION;
private final String NAME;
private final ISuccess SUCCESS;
private final IFailure FAILURE;
private final IError ERROR;
public DownloadHandler(String url, HashMap params,
IRequest request, String downloadDir,
String extension, String name,
ISuccess success, IFailure failure,
IError error) {
this.URL = url;
this.PARAMS = params;
this.REQUEST = request;
this.DOWNLOAD_DIR = downloadDir;
this.EXTENSION = extension;
this.NAME = name;
this.SUCCESS = success;
this.FAILURE = failure;
this.ERROR = error;
}
//处理文件下载
public final void handleDownload() {
if (REQUEST != null) {
REQUEST.onRequestStart();
}
RestCreator.getRestService().download(URL, PARAMS)
.enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) { //成功时的回调
final ResponseBody responseBody = response.body();
final SaveFileTask task = new SaveFileTask(REQUEST, SUCCESS);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
DOWNLOAD_DIR, EXTENSION, responseBody, NAME);
//这里一定要注意判断,否则文件下载不全
if (task.isCancelled()) {
if (REQUEST != null) {
REQUEST.onRequestEnd();
}
}
} else { //错误时的回调
ERROR.onError(response.code(), response.message());
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull Throwable t) {
if (FAILURE != null)
FAILURE.onFailure();
}
});
}
}
3、添加到构建者模式
我们通过和之前一样的方法,在RestClient类中新增下载文件的一些参数,并且在构造方法中补上:
private final File FILE; //文件
private final String DOWNLOAD_DIR; //文件目录
private final String EXTENSION; //后缀名
private final String NAME; //文件名
还要提供一个下载的调用方法,内部实现直接构造一个DownloadHandler对象即可:
//下载文件
public final void download() {
new DownloadHandler(URL, PARAMS, REQUEST, DOWNLOAD_DIR,
EXTENSION, NAME, SUCCESS, FAILURE, ERROR)
.handleDownload();
}
并且在RestClientBuilder类中提供相对应的构建方法:
public final RestClientBuilder file(File file) {
this.mFile = file;
return this;
}
public final RestClientBuilder file(String filePath) {
this.mFile = new File(filePath);
return this;
}
public final RestClientBuilder dir(String dir) {
this.mDownloadDir = dir;
return this;
}
public final RestClientBuilder extension(String extension) {
this.mExtension = extension;
return this;
}
public final RestClientBuilder name(String name) {
this.mName = name;
return this;
}
好了到这里,我们的整个框架就已经封装完成了,不过还是有很多需要完善的地方,我只能日后再说了!
四、编写测试案例
下面在应用层写一个测试案例,来测试框架是否能够正常使用,这里我就只贴实现的核心代码了(文章太长估计都没耐心看了):
接口地址(猫眼电影(非官方)):http://m.maoyan.com/movie/list.json?type=hot&offset=0&limit=10
请求代码如下:
RestClient.Builder()
.context(this)
.url("movie/list.json?type=hot")
.params("offset", 0)
.params("limit", 10)
.clazz(MovieBean.class)
.listener(new ISuccess() {
@Override
public void onSuccess(Object response) {
MovieBean bean = (MovieBean) response;
mList.addAll(bean.getData().getMovies());
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
recyclerView.setAdapter(new IndexListAdapter(MainActivity.this, mList));
}
})
.listener(new IError() {
@Override
public void onError(int code, String msg) {
Log.e("onError: ", msg);
}
})
.listener(new IFailure() {
@Override
public void onFailure() {
Log.e("onFailure: ", "请求失败");
}
})
.build()
.get();
最终实现的效果图如下图所示:
好了,写到这里就要结束了,最后再甩一遍本项目的地址:https://github.com/JArchie/NetTest
最后祝大家新年快乐,阖家幸福!