真实案例出发,再谈retrofit封装
原文链接:Anthony的简书博客
项目代码:CameloeAnthony/Ant
前言
在使用了一段时间的Retrofit之后,今天终于在这里讲解到了网络的部分。目前开源的HTTP 框架有很多,Volley,Android Async Http,以及OkHttp +Retrofit等。而我在自己的使用中选择了Retrofit,这里就从基础到原理,再到实例的方式,讲解我对Retrofit做出的一些封装和使用。来让你进一步的了解和掌握Retrofit的使用。
基础
Retrofit一个基于OkHttp的RESTFUL API请求工具。它是 Square 推出的 HTTP 框架,主要用于 Android 和 Java。Retrofit 将网络请求变成方法的调用,使用起来非常简洁方便。
A type-safe HTTP client for Android and Java
如果你还对Retrofit不了解,那么我建议你去官方文档了解一下。
Retrofit使用大体分为三个步骤
(1)Retrofit将HTTP API 转化成了Java接口的形式,所以首先我们会提供一个接口类GitHubService 。
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
(2)Retrofit类可以针对之前定义的GitHubService 接口生成一个具体实现。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
(3)然后就可以对GitHubService 的方法进行同步或者异步的调用来进行网络的访问,也就是说可以通过call对象获得数据:(可以使用enqueue 或者 execute来执行发起请求,enqueue是是异步执行,而 execute是同步执行。)
Call<List<Repo>> repos = service.listRepos("octocat");
通过上面三个步骤,我们会发现Retrofit给人眼前一亮的当然是它的注解调用和优雅的API转化为方法。每一个方法都会对应着一个Http的注解,总共有GET, POST, PUT, DELETE,HEAD五个内嵌的注解。我们也会在注解上指定相应的相对地址信息。比如上方的@GET("users/{user}/repos")
这里本来想将官网所有内容翻译一遍的,返现很多词不达意 。然后今天又凑巧看到了郭神公众号推荐的一篇文章 Android网络请求库 - Say hello to retrofit.对官网对的内容讲解得非常的详细易懂,继续阅读下面章节之前,一定要去看看这篇文章。
于是我们完整的Retrofit使用流程:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
//.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> repos = service.listRepos("octocat");
repos.enqueue(new Callback<List<Repo>>() {
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
}
});
Retrofit原理解析
在进一步了解和使用Retrofit之前,不妨先来了解Retrofit的原理,看看Retrofit的源码是了解原理的一个有效途径。
(1) 源码结构
Retrofit包含一个http包,里面全部是定义HTTP请求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等。
余下的retrofit包中几个类和接口就是全部retrofit的代码了,代码很少因为retrofit把网络请求这部分功能全部交给了OkHttp了。
(2) 整体流程
继续回到官网的例子。
首先关注的是我们通过new Retrofit.Builder()...build()
进行Retrofit的构建,可以了解的是这里使用的是 Builder 模式。
在Android源码中,经常用到Builder模式的可能就是AlerDialog 了。Builder模式用于将一个复杂的对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。这里Retrofit使用Builder模式支持了支持不同的转换(就是将HTTP返回的数据解析成Java对象,主要有Xml、Gson等)和返回(主要作用就是将Call对象转换成另一个对象,比如RxJava)。这里也就真正的达到了构建复杂对象和它的部件进行解耦。
这里通过build方法来了解Retrofit创建,需要6个参数。如下方代码注解:
public Retrofit build() {
//1 baseUrl 基地址
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}
//2 callFactory 默认创建一个 OkHttpClient
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
//3 callbackExecutor Android 中返回的是 MainThreadExecutor
callbackExecutor = platform.defaultCallbackExecutor();
}
//4 adapterFactories(比如RxJavaCallAdapterFactory 用于将Call返回支持Rxjava) 把Call对象转换成其它类型
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
//5 converterFactories(例如GsonConverterFactory 用于Gson转换) 请求网络得到的response的转换器的集合 默认会加入 BuiltInConverters ,
List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);
//6 private boolean validateEagerly; validateEagerly 是否需要立即解析接口中的方法
return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly);
}
}
所以我们会看到我们通过Builder模式创建Retrofit访问对象都必须指定基地址url。如果还需要支持Gson转换,我们就需要添加.addConverterFactory(GsonConverterFactory.create())
,如果需要支持Rxjava,那么还需要添加 .addCallAdapterFactory (RxJavaCallAdapterFactory.create())
。
接着我们通过GitHubService service = retrofit.create(GitHubService.class);
create方法创建网络请求接口类GitHubService 的实例。我们正是使用该对象的listRepos方法完成了Call<List<Repo>> repos = service.listRepos("octocat");
获取到了数据。下面看看create方法的源码:
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
//为了兼容 Java8 平台,Android 中不会执行
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
create方法接受一个 Class 对象,也就是我们编写的接口,里面含有通过注解标识的请求网络的方法。注意 return
语句部分,这里调用了 Proxy.newProxyInstance方法,这个很重要,因为用了动态代理模式。关于动态代理模式,可以参考这篇文章:公共技术点之 Java 动态代理。简单的描述就是,Proxy.newProxyInstance根据传进来的 Class 对象生成了一个实例 A,也就是代理类。每当这个代理类 A 执行某个方法时,总是会调用 InvocationHandler(Proxy.newProxyInstance中的第三个参数) 的invoke方法,在这个方法中可以执行一些操作(这里是解析方法的注解参数等),通过这个方法真正的执行我们编写的接口中的网络请求。
也就是概括一句话:通过动态代理的方式把 Java 接口中的解析为响应的网络请求,然后交给 OkHttp 去执行。并且可以适配不同的 CallAdapter
,可以方便与 RxJava 结合使用。
更多源码细节,参考这篇文章。Android Retrofit源码解析
封装和使用
之前有网友评论,说网络上的很多开源库已经封装的很完美了 ,我们就不需要再次做出多余的封装了 。这个观点实在是不敢苟同,开源库固然已经做了很多事情,但是我们还是要根据不同的业务逻辑封装自己的使用呢 。比如同样的图片加载,我们不可能每次都要调用Glide的一些初始化操作。同样的网络请求,我们也不可能每次都写一大堆初始化代码 。每个app的逻辑业务操作都是相同的,当然可以封装起来,让代码更加清爽。
下面讲讲我这里的封装逻辑。并提供Github我的关注列表以及百度天气接口的访问,两个真实案例进行讲解使用,项目代码将会在CameloeAnthony/Ant 中提供更新:
(1) Github 关注列表
来看看Github访问页面,这里只需要下面几行代码就完成了GithubUser 数据的返回。
mSubscription = mDataManager.loadUserFollowingList("CameloeAnthony")
.subscribe(new HttpSubscriber<List<GithubUser>>() {
@Override
public void onNext(List<GithubUser> users) {
......Github用户数据加载完成
}
});
这里使用loadUserFollowingList
方法通过Rxjava的Observable返回Observable<List<GithubUser>>
对象,DataManager是一个数据的入口,我们不将所有的数据访问放在DataManager中。这种创建方式在之前的文章浅析MVP中model层设计中有过提及,类似于通常使用的 Respository
接下来看看DataManager中的loadUserFollowingList
方法。
/**
* load following list of github users
* @return Observable<List<GithubUser>>
*/
public Observable<List<GithubUser>> loadUserFollowingList(String userName){
return mHttpHelper.getService(GithubApi.class)
.loadUserFollowingList(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
这里传入了接口GithubApi.class 然后调用了HttpHelper的loadUserFollowingList
方法。
在此架构中,Model层被划分为两个部分:许多helpers类和一个 DataManager
.helpers类的数量在不同的工程中不尽相同,但是每个都有自己的功能。比如:通过SharedPreferences与数据进行交互的PreferHelper,通过SqlBrite提供与数据库交互的DatabaseHelper,DataManager结合并且转化不同的Helpers类为Rx操作符,向Presenter层提供Observables类型的数(provide meaningful data to the Presenter),并且同时处理数据的并发操作(group actions that will always happen together.)。这一层也包含实际的model类,用于定义当前数据架构。 -------摘选自浅析MVP中model层设计
GithubApi 接口,你可以直接访问
https://api.github.com/users/CameloeAnthony/following 获取到这些列表数据。
public interface GithubApi {
String end_point = "https://api.github.com/";
@GET("/users/{user}/following")
Observable<List<GithubUser>> loadUserFollowingList(@Path(value = "user") String user);
}
GithubUser 是与Github API对应的Github用户信息的实体类,API 和实体类的转化可以去网站http://www.jsonschema2pojo.org/ 快捷完成:
public class GithubUser {
@SerializedName("login")
private String login;
@SerializedName("id")
private Integer id;
......
public String getLogin() {
return login;
}
......
(2) 天气信息的加载
这里加载百度API提供的天气信息
首先还是加载的页面的方法,非常简单的Rxjava操作完成了数据的读取
mSubscription = mDataManager.loadWeatherData(“成都”).subscribe(new HttpSubscriber<WeatherData>() {
@Override
public void onNext(WeatherData weatherData) {
.......天气数据加载完成
}
@Override
public void onError(Throwable e) {
super.onError(e);
toastUtils.showToast("加载天气信息失败");
}
});
接着看看DataManager提供的方法 loadWeatherData
public Observable<WeatherData> loadWeatherData(String location) {
Map<String, String> params = new HashMap<>();
params.put("location", location);
params.put("language", "zh-Hans");
params.put("unit", "c");
params.put("start", "0");
params.put("days", "3");
return mHttpHelper.getService(WeatherApi.class)
.loadWeatherData(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
这里同样是调用了HttpHelper的loadWeatherData方法。遵从的是上面一样的Model层访问原则。所有数据都是先访问DataManager再访问相应的类,比如这里的HttpHelper。
public interface WeatherApi {
String end_point = “http://apis.baidu.com/”;
//example , remember to add a apikey to your header
// "http://apis.baidu.com/thinkpage/weather_api/suggestion?location=beijing&language=zh-Hans&unit=c&start=0&days=3";
@Headers("apikey: 87f4cacc3ffe1f1025ebf1ea415ff112")
@GET("/thinkpage/weather_api/suggestion")
Observable<WeatherData> loadWeatherData(@QueryMap Map<String,String> params);
}
WeatherData同样是根据百度天气API编写的实体类 。这个实体类也是有点复杂。所以同样是通过http://www.jsonschema2pojo.org/ 把API json放到输入框,然后写好名字,快速的完成了实体类的创建。
所以这里就讲解完了两个接口的调用 。但是客官,你肯定要说 ,这不对呀 ,你这Retrofit踪迹都没看到。你就完成了各种API调用,你逗我呢吧?哈哈,接着往下看。
(3)retrofit封装
这里还是要回到最初的那一段代码,retrofit的创建分为三个步骤。回到基础部分再看一下吧。虽然上面的两次API调用都没有“使用Retrofit”,但是都是使用了HttpHelper类。将GithubApi
和WeatherApi
分别传递到了HttpHelper对象的getService
方法中,所以猫腻就在这里 。看下面的代码:
/**
* Created by Anthony on 2016/7/8.
* Class Note:
* entrance class to access network with {@link Retrofit}
* used only by{@link DataManager} is recommended
* <p>
* 使用retrofit进行网络访问的入口类,推荐只在{@link DataManager}中使用
*/
public class HttpHelper {
private static final int DEFAULT_TIMEOUT = 30;
private HashMap<String, Object> mServiceMap;
private Context mContext;
// private Gson gson = new GsonBuilder().setLenient().create();
@Inject
public HttpHelper(@ApplicationContext Context context) {
//Map used to store RetrofitService
mServiceMap = new HashMap<>();
this.mContext = context;
}
@SuppressWarnings("unchecked")
public <S> S getService(Class<S> serviceClass) {
if (mServiceMap.containsKey(serviceClass.getName())) {
return (S) mServiceMap.get(serviceClass.getName());
} else {
Object obj = createService(serviceClass);
mServiceMap.put(serviceClass.getName(), obj);
return (S) obj;
}
}
@SuppressWarnings("unchecked")
public <S> S getService(Class<S> serviceClass, OkHttpClient client) {
if (mServiceMap.containsKey(serviceClass.getName())) {
return (S) mServiceMap.get(serviceClass.getName());
} else {
Object obj = createService(serviceClass, client);
mServiceMap.put(serviceClass.getName(), obj);
return (S) obj;
}
}
private <S> S createService(Class<S> serviceClass) {
//custom OkHttp
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
//time our
httpClient.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
httpClient.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
httpClient.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
//cache
File httpCacheDirectory = new File(mContext.getCacheDir(), "OkHttpCache");
httpClient.cache(new Cache(httpCacheDirectory, 10 * 1024 * 1024));
//Interceptor
httpClient.addNetworkInterceptor(new LogInterceptor());
httpClient.addInterceptor(new CacheControlInterceptor());
return createService(serviceClass, httpClient.build());
}
private <S> S createService(Class<S> serviceClass, OkHttpClient client) {
String end_point = "";
try {
Field field1 = serviceClass.getField("end_point");
end_point = (String) field1.get(serviceClass);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.getMessage();
e.printStackTrace();
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(end_point)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(client)
.build();
return retrofit.create(serviceClass);
}
private class LogInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
Timber.i("HttpHelper" + String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
Timber.i("HttpHelper" + String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
// log Response Body
// if(BuildConfig.DEBUG) {
// String responseBody = response.body().string();
// Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s%n%s",
// response.request().url(), (t2 - t1) / 1e6d, response.headers(), responseBody));
// return response.newBuilder()
// .body(ResponseBody.create(response.body().contentType(), responseBody))
// .build();
// } else {
// Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s",
// response.request().url(), (t2 - t1) / 1e6d, response.headers()));
// return response;
// }
}
}
private class CacheControlInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!AppUtils.isNetworkConnected(mContext)) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
if (AppUtils.isNetworkConnected(mContext)) {
int maxAge = 60 * 60; // read from cache for 1 minute
response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
return response;
}
}
}
这里getService方法将会获取缓存中的是否有传递进来的Class对象。有则使用,没有则创建。
这里则调用了createService(Class<S> serviceClass)
进行了OkHttpClient的初始化操作,并添加了两个LogInterceptor,CacheControlInterceptor 分别用于打印相关的请求信息。最终调用的方法是createService(Class<S> serviceClass, OkHttpClient client)
我们通过反射达到了end_point字段的基地址。比如上面的https://api.github.com/ 和 http://apis.baidu.com/。
然后代码也就回到了
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(end_point)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(client)
.build();
return retrofit.create(serviceClass);
可以看到了这里同样是进行了Retrofit的Builder创建以及create操作。到这里我想你就.addConverterFactory(GsonConverterFactory.create())
完成了添加Gson的转换器的操作。RxJavaCallAdapterFactory.create()
完成了RxJava结果返回的支持。所以我们才可以支持了返回Observable的对象。
(4) 思路梳理
这里所有的代码也就完成了我们的操作。所以能够看到网络的访问变得更加简洁。这里对优点和缺点进行总结,需要在开发使用中注意:
优点:
1 通过DataManager作为数据入口的形式,屏蔽底层细节,让网络的访问更加清晰。
2 使用http://www.jsonschema2pojo.org/ 网站快速的通过API Json数据完成了实体类的创建
3 支持Rxjava的流式操作,是Retrofit的使用更加得心应手。当我们执行异步操作的时候,java提供了Thread, Future,FutureTask, CompletableFuture 去解决这个问题,但是随着任务的复杂程度的增加,代码也变得难于维护,他们也不能实现Rxjava一样的链式处理操作。Rxjava相比具有更高的灵活性,可以链式调用,可以对单个事件以及序列进行操作。
4 支持Gson转换器和Retrofit的配合,省去了Gson fromJson的操作。更加便捷。
5 HttpHelper对Retrofit的封装省去了Retrofit初始化的创建,并且添加了拦截器进行日志打印方便查看。
缺点:
1 Api接口类中的基地址,必须按照"end_point"的形式提供。
2 DataManager随着项目的增大作为唯一的数据入口将会变得越来越臃肿。
当然本项目也引入了Dagger2和ButterKnife,让代码更加的整洁易用。
当然本篇文章没有对官网和源码细节进行进一步解析。都可以在上面和下方提供的参考链接中进行查看。更多代码细节请查看
CameloeAnthony/Ant
example project make your architecting of android apps quicker and smoother ,long-term maintenance by me,blog updating at the same time.Used in real project in my development
由作者长期维护的架构以及示例代码,用于本人的各种真实项目。博客更新,希望对你的安卓架构提供指导性的意义
参考文章
Retrofit官网
Android网络请求库 - Say hello to retrofit
RxJava 与 Retrofit 结合的最佳实践
「Android技术汇」Retrofit2 源码解析和案例说明
Android Retrofit源码解析
Rxjava+ReTrofit+okHttp深入浅出-终极封装