真實案例出發,再談retrofit封裝
前言
在使用了一段時間的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("
GitHubService service = retrofit.create(GitHubService.class);</code></pre>
(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("
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) {
}
});</code></pre>
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);
}
}</code></pre>
所以我們會看到我們通過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);
}
});
}</code></pre>
create方法接受一個 Class 對象,也就是我們編寫的接口,里面含有通過注解標識的請求網絡的方法。注意 return
語句部分,這里調用了 Proxy.newProxyInstance方法,這個很重要,因為用了 動態代理模式 。關于動態代理模式,可以參考這篇文章: 公共技術點之 Java 動態代理 。簡單的描述就是,Proxy.newProxyInstance根據傳進來的 Class 對象生成了一個實例 A,也就是代理類。每當這個代理類 A 執行某個方法時,總是會調用 InvocationHandler(Proxy.newProxyInstance中的第三個參數) 的invoke方法,在這個方法中可以執行一些操作(這里是解析方法的注解參數等),通過這個方法真正的執行我們編寫的接口中的網絡請求。
也就是概括一句話:通過動態代理的方式把 Java 接口中的解析為響應的網絡請求,然后交給 OkHttp 去執行。并且可以適配不同的 CallAdapter
可以方便與 RxJava 結合使用。
封裝和使用
之前有網友評論,說網絡上的很多開源庫已經封裝的很完美了 ,我們就不需要再次做出多余的封裝了 。這個觀點實在是不敢茍同,開源庫固然已經做了很多事情,但是我們還是要根據不同的業務邏輯封裝自己的使用呢 。比如同樣的圖片加載,我們不可能每次都要調用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());
}</code></pre>
這里傳入了接口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類,用于定義當前數據架構。

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字段的基地址。
然后代碼也就回到了
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 使用 快速的通過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,讓代碼更加的整潔易用。
當然本篇文章沒有對官網和源碼細節進行進一步解析。都可以在上面和下方提供的參考鏈接中進行查看。
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 由作者長期維護的架構以及示例代碼,用于本人的各種真實項目。博客更新,希望對你的安卓架構提供指導性的意義
參考文章
Android網絡請求庫 - Say hello to retrofit
RxJava 與 Retrofit 結合的最佳實踐 「Android技術匯」Retrofit2 源碼解析和案例說明 Android Retrofit源碼解析 Rxjava+ReTrofit+okHttp深入淺出-終極封裝
來自:http://blog.csdn.net/u014315849/article/details/52809011