帶你學開源項目:Meizhi Android之RxJava & Retrofit最佳實踐
零、背景
比起閱讀枯燥的技術文檔,獨自苦苦摸索新技術的基本用法,還有一種更好更快速也更有效的提高自身技術的方法,那就是閱讀學習優質的開源項目,通過仿寫、練習最終達到理解,潛移默化提升自身編程技能。
《帶你學開源項目》系列將帶領你深入閱讀及分析當前流行的一些開源項目,并針對其中采用的新技術與精妙之處進行細致的闡述,以期讓你快速掌握Android開發中的多種強大技能點。
一、本期開源項目Meizhi Android
本次的開源項目選擇了 Meizhi Android ,本文主要介紹該項目中采用的 RxJava 、 Retrofit 兩種技術,這二者在Android開發者中非常流行,不僅能夠 優美地處理異步回調 ,而且能 提高代碼的性能和穩定性 。而Meizhi Android中較好的覆蓋了二者的多種應用場景,能夠給多數開發者一個全面的學習。
下面本人會 對原項目的代碼進行詳細的介紹 ,同時為了讀者看的清楚其中的邏輯關系,可能會做一定調整以幫助讀者理解,比如把lambda表達式還原成普通java函數形式,以避免很多讀者對lambda并不熟悉。
二、原項目分析
0. clone項目到本地
第一步當然是把項目clone下來,編譯,運行。有興趣的同學可以執行這一步。
1. 添加 Stetho 抓包工具
首先,由于我們要分析retrofit,所以為了查看app的網絡請求,有興趣的同學可以手動在代碼里添加 Stetho 。 Stetho 是非死book推出的一款黑科技,能夠在chrome里輕松查看app所有的網絡請求,比起iOS需要裝個 Charles 查看http請求方便多咯。
Stetho使用場景
2. Retrofit結構
從下圖我們可以看到,首頁里有很多card,每一個card里有兩個元素: 妹紙圖片 , 描述文字 ,具體UI實現我們不在乎,只要明白一點,這兩個元素數據是來自于兩個不同的api。其中, 妹紙圖片 來自于 http://gank.io/api/data/福利/10 ; 描述文字 來自于 http://gank.io/api/data/休息視頻/10 。
app中為了請求網絡數據,采用了 Retrofit 。具體關于retrofit如何配置請各位參考官網,這里只講解如何使用 Retrofit 。
該項目中主要創建了以下幾個類來實現 Retrofit 結構,大家可以作為參考用于自己的項目中。
i. GankApi :這個類用來定義相關的 http 接口,這是符合retrofit規范的定義形式,每一個api返回的為 Observable<T> 格式結果,方便 RxJava 進行進一步處理。
@GET("/data/福利/{page}") Observable<MeizhiList> getMeizhiList(@Path("page") int page);
@GET("/data/休息視頻/{page}") Observable<GankVideoList> getGankVideoList(@Path("page") int page);
ii. DrakeetRetrofit :這個類用來對 Retrofit 進行相關配置并生成 GankApi 實例 gankApi
OkHttpClient client = new OkHttpClient();
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new OkClient(client))
.setLogLevel(RestAdapter.LogLevel.FULL)
.setEndpoint("http://gank.io/api")
.setConverter(new GsonConverter(gson));
RestAdapter gankRestAdapter = builder.build();
GankApi gankApi = gankRestAdapter.create(GankApi.class);
public GankApi getGankApi() {
return gankApi;
}
iii. DrakeetFactory : 這個類用來對外生成單例 GankApi 實例,為確保 GankApi 實例只生成一次。
public static GankApi getGankApi() {
synchronized (monitor) {
if (sGankApi == null) {
sGankApi = new DrakeetRetrofit().getGankApi();
}
return sGankApi;
}
}
所以,在實際應用場景中,比如我們想要發起一個http請求來獲取 福利 數據,那么我們可以采用以下方式:
GankApi gankApi = DrakeetFactory.getGankApi();
Observable<MeizhiList> meizhiList = gankApi. getMeizhiList(10);
首頁.png
3. 首頁的RxJava的實現
既然我們已經把網絡框架搭建好了,那么可以開始從服務器獲取數據并顯示了。我們首先看首頁的數據。下面,我來對首頁數據進行分析,一步步推出所需要的RxJava表達式。
上面已經介紹過,每一個card里有兩部分數據: 妹紙圖片 (紅色方框)和 描述文本 (綠色方框)。
- 妹紙圖片 數據來自于 "/data/福利/{page}" 這個api,該api會返回妹紙圖片的url;
- 描述文本 來自于 "/data/休息視頻/{page}" 這個api,該api會返回休息視頻及相關描述信息,card里會把描述信息顯示出來;
- 兩個api均可以攜帶 page 字段,即一次請求可以獲得多個數據。如我們在 "/data/福利/{page}" 里設置 page=10 ,那么我們一次請求可以得到10條 福利 數據,即 10張妹紙圖片url ;
- 由于我們一次可以獲得多張妹紙圖片url和多個視頻信息,那我們就需要把 二者進行合并 ,即 單拎出來一張妹紙圖片和一個視頻信息組裝成一個card 。然后按這種方式生成其他的card。
小結一下,根據以上描述,假如我們把兩個api的page都設置為 10 ,那么兩個請求同時發出去后,我們能得到 10張妹紙圖片url (如 http://img.com/1.png , http://img.com/2.png , ...)和 10個視頻信息 (如 舌尖上的中國 , 星際穿越 , ...),然后我們將二者組裝成 10個card所需要的數據 ,放入每個card里顯示即可。
好,終于可以開始動手寫代碼了。上面的分析看似復雜,然后只要你學會了如何分析,很快就能寫出對應的RxJava代碼。下面我結合RxJava的 數據流思想 和 具體操作符 來介紹實現代碼。
i. 在網絡請求數據之前,我們要創建幾個數據entry對象來將獲取回來的json字符串轉化為object
public class Meizhi {
public String url;
public Date publishDate;
} //這是一個Meizhi對象,存儲妹紙圖片的url,圖片描述信息和創建日期
public class Video {
public String desc;
public Date publishDate;
} //這是一個視頻對象,存儲視頻描述信息和創建日期
public class MeizhiList {
public List<Meizhi> meizhiList;
} //由于我們一次請求能獲取到10個(根據`page`設置),所以我們用MeizhiList來存儲結果
public class VideoList {
public List<Video> videoList;
} //原理同上,存儲多個video對象
public class MeizhiWithVideo {
public String url;
public String desc;
public Date publishDate;
}//將video信息合并入meizhi對象中
public class MeizhiWithVideoList {
public List<MeizhiWithVideoList> data;
}
ii. zip: 將兩個retrofit接口請求后得到的兩個數據源Observable<MeizhiList> Observable<VideoList>進行合并
我們需要把這兩個數據源的數據拼接起來,所以我們可以考慮使用 zip操作符 ,該操作符可以將兩個數據源發射出來的數據依次組裝在一起。
比如一個 Observable數據源 依次發射出 1, 3, 5, 7 , 另一個 Observable數據源 依次發射出 a, b, c, d ,那么 zip操作符 組裝后會對外發射出 1a, 3b, 5c, 7d 這樣的數據。
而我們需要的正是這樣。
Observable<MeizhiList> 一次對外發射一個 MeizhiList 對象, Observable<VideoList> 一次對外發射一個 VideoList 對象,我們將二者合并成一個 MeizhiWithVideoList 對象。然后把 MeizhiWithVideoList 對象拿給UI去進行顯示即可。
所以,我們可以得到:
Observable<MeizhiList> meizhiListObservable = gankApi.getMeizhiList(10);
Observable<VideoList> videoListObservable = gankApi.getVideoList(10);
Observable<MeizhiWithVideoList> meizhiWithVideoListObservable =
Observable.zip(meizhiListObservable, videoListObservable, this::mergeVideoWithMeizhi)
其中 mergeVideoWithMeizhi 是一個合并函數,把 video 信息與 meizhi 信息合并成新的 MeizhiWithVideo對象 。
public MeizhiWithVideoList
mergeVideoWithMeizhi(MeizhiList meizhiList, VideoList videoList) {//省略...}
RxJava - zip
iii. 對MeizhiWithVideo對象進行排序。
在上面,我們通過合并,得到了 Observable<MeizhiWithVideoList> 數據源,這個數據源對外發射出一個 MeizhiWithVideoList 對象,這個對象里有10個 MeizhiWithVideo 數據,我們可以對這10個數據利用它們的發布日期進行排序。
所以我們要實現以下幾步:
-
先把 Observable<MeizhiWithVideoList> 數據源轉化為 Observable<List<MeizhiWithVideo>> ,從對外發一個 MeizhiWithVideoList 對象變成對外發射一個 List<MeizhiWithVideo> 對象;
-
再把 Observale<List<MeizhiWithVideo>> 轉化為 Observable<MeizhiWithVideo> 數據源,變成了對外發射出10個 MeizhiWithVideo 對象;
-
對這10個 MeizhiWithVideo 對象基于 publishDate 進行排序;
-
其中比較操作很耗cpu,所以我們放在 Schedulers.computation() 線程中做
代碼實現:
meizhiWithVideoListObservable.map(new Func1<MeizhiWithVideoList, List<MeizhiWithVideo>>() {
@Override
public List<Meizhi> call(MeizhiList meizhiList) {
return MeizhiWithVideoList.data;
}
})
.flatMap(new Func1<List<MeizhiWithVideo>, Observable<MeizhiWithVideo>>() {
@Override
public Observable<MeizhiWithVideo> call(List<MeizhiWithVideo> meizhiWithVideos) {
return Observable.from(meizhiWithVideos);
}
})
.toSortedList(new Func2<MeizhiWithVideo, MeizhiWithVideo, Integer>() {
@Override
public Integer call(MeizhiWithVideo meizhiWithVideo1, MeizhiWithVideo meizhiWithVideo2) {
return meizhiWithVideo2.publishedAt.compareTo(meizhiWithVideo1.publishedAt);
}
})
.subscribeOn(Schedulers.computation());
iv. 排序后,我們得到Observable<List<MeizhiWithVideo>>數據源,傳給adapter去更新UI
上面的 toSortedList(xxx) 方法會把 Observable<MeizhiWithVideo> 排序后重新組裝成 Observable<List<MeizhiWithVideo>> 對象 sortedMVListObservable ,該對象對外發射一個 有序的List<MeizhiWithVideo> 。我們將該數據源提供給adapter供顯示。
代碼如下:
sortedMVListObservable.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<List<MeizhiWithVideo>>() {
@Override
public void onCompleted() {
setRefresh(false); // stop refreshing data.
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(List<MeizhiWithVideo> meizhiWithVideoList) {
adapter.setData(meizhiWithVideoList);
adapter.notifyDataSetChanged(); // update UI
}
})
4. 利用 Subscription 來管理異步處理與Activity生命周期
對于異步我們知道一直存在一個問題,假設一個頁面要同時發出很多個http請求,如http1, http2, http3...,然后這些請求會被放在一個隊列里依次發出,而且每個請求發出后需要等待一段時間才能得到返回數據。
那么問題就來了,假設在A頁面發出了多個網絡請求,在這些網絡請求還在等待響應時用戶就跳轉到了B頁面,在以前的情況下是,A頁面的網絡請求仍然進行直到所有數據返回,而且當數據返回時會嘗試去調用A頁面的UI進行修改,而此時已經進入了B頁面,所以,這不僅造成了網絡資源的浪費,也存在一定的風險。
有了RxJava,我們可以把每一個網絡請求轉化為一個 Subscription 對象,這個 Subscription 對象可以被手動 unsubscribe ,即停止訂閱所請求的數據源,這樣就可以暫定數據請求,而且即使數據返回回來,由于我已經取消訂閱了,所以不會再接收到這些數據了。
代碼實現:
在 BaseActivity 中,創建一個 CompositeSubscription 對象來進行管理
`BaseActivity`
private CompositeSubscription mCompositeSubscription;
protected void addSubscription(Subscription s) {
if (this.mCompositeSubscription == null) {
this.mCompositeSubscription = new CompositeSubscription();
}
this.mCompositeSubscription.add(s);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (this.mCompositeSubscription != null) {
this.mCompositeSubscription.unsubscribe();
}
}
在實際的Activity中的網絡請求:
public class MyActivity extends BaseActivity {
private void loadData() {
Subscription s = gankApi.getMeizhiList(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...);
addSubscription(s);
}
}
三、改進及總結
本文通過對開源項目 Meizhi Android 進行分析,了解了 Retrofit , RxJava 的實際應用場景,也對于二者有了更加深入的認識。
不過本人認為該項目還有一些可以改善的地方,比如 Retrofit 中利用 DrakeetFactory 工廠來生成 GankApi 的單例,但是 new DrakeetRetrofit().getGankApi(); 也是一個可以生成 GankApi 的方法,而且是 public 的,那么如果新的開發者忘記調用 DrakeetFactory 來生成 GankApi 的實例,而是采用后者,那么工廠模式就達不到預期的目的了。我認為可以把 new DrakeetRetrofit().getGankApi(); 這個操作內容放在 DrakeetFactory 工廠內部,并且設置為 private 屬性,這樣的話如果想要獲得 GankApi 實例,就必須依靠 DrakeetFactory 來生成,從而真正保證了 單例 的優勢。
最后,如果讀者有意見歡迎評論,本人后續還會挑選優質的開源項目,分析其精髓,供讀者學習領悟。
謝謝!
wingjay