写在前面
之前基于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<String> get(@Url String url, @QueryMap Map<String, Object> params); //Post请求 @FormUrlEncoded @POST Call<String> post(@Url String url, @FieldMap Map<String, Object> params); //Post原始数据 @POST Call<String> postRaw(@Url String url, @Body RequestBody body); //Put请求 @FormUrlEncoded @PUT Call<String> put(@Url String url, @FieldMap Map<String, Object> params); //Put原始数据 @PUT Call<String> putRaw(@Url String url, @Body RequestBody body); //Delete请求 @DELETE Call<String> delete(@Url String url, @QueryMap Map<String, Object> params); //文件下载 @Streaming @GET Call<ResponseBody> download(@Url String url, @QueryMap Map<String, Object> params); //文件上传 @Multipart @POST Call<String> 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<String> { 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<String> call, @NonNull Response<String> 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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> 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<String> 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<Object, Void, File> { private final IRequest REQUEST; private final ISuccess SUCCESS; public SaveFileTask(IRequest request, ISuccess success) { this.REQUEST = request; this.SUCCESS = success; } @Override protected File doInBackground(Object... params) { String downloadDir = (String) params[0]; String extension = (String) params[1]; final ResponseBody body = (ResponseBody) params[2]; final String name = (String) params[3]; final InputStream is = body.byteStream(); //获得输入流 if (downloadDir == null || downloadDir.equals("")) { downloadDir = "down_loads"; //下载的文件目录 } if (extension == null || extension.equals("")) { extension = ""; } if (name == null) { return FileUtil.writeToDisk(is, downloadDir, extension.toUpperCase(), extension); } else { return FileUtil.writeToDisk(is, downloadDir, name); } } @Override protected void onPostExecute(File file) { super.onPostExecute(file); if (SUCCESS != null) { SUCCESS.onSuccess(file.getPath()); } if (REQUEST != null) { REQUEST.onRequestEnd(); } autoInstallApk(file); } //下载apk文件下载完成时自动安装程序 private void autoInstallApk(File file) { if (FileUtil.getExtension(file.getPath()).equals("apk")) { final Intent install = new Intent(); install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); install.setAction(Intent.ACTION_VIEW); install.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); Contract.getContext().startActivity(install); } } }2、处理下载逻辑及回调
新建一个类DownloadHandler,然后定义好网络请求的URL、参数、文件目录、后缀名、文件名、相关回调接口,最后通过RestService调用内部定义的download方法,实现Callback,泛型传入ResponseBody,在重写的成功和失败的回调方法里进行具体的实现,成功的回调中通过我们上面定义的SaveFileTask类的对象去调用下载的执行方法进行真正的下载,这里调用的是executeOnExecutor方法,它通常和THREAD_POOL_EXECUTOR一起使用,允许多个任务在由AsyncTask管理的线程池中并行执行,代码如下:
public class DownloadHandler { private final String URL; private final HashMap<String, Object> 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<String, Object> 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<ResponseBody>() { @Override public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> 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<ResponseBody> 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
最后祝大家新年快乐,阖家幸福!