版權聲明:本文原創發佈於公衆號 wingjay,轉載請務必註明出處! https://blog.csdn.net/lanxian837820149/article/details/88629021
大幅提高自身技術實力最有效的途徑之一就是學習世界級優秀開源項目的精髓,而本人的《帶你學開源項目》系列文章將持續更新,對當前Android開發界最優秀的開源項目進行深入分析。
零、背景
比起閱讀枯燥的技術文檔,獨自苦苦摸索新技術的基本用法,還有一種更好更快速也更有效的提高自身技術的方法,那就是閱讀學習優質的開源項目,通過仿寫、練習最終達到理解,潛移默化提升自身編程技能。
《帶你學開源項目》系列將帶領你深入閱讀及分析當前流行的一些開源項目,並針對其中採用的新技術與精妙之處進行細緻的闡述,以期讓你快速掌握Android開發中的多種強大技能點。
一、本期開源項目Meizhi Android
本次的開源項目選擇了Meizhi Android,本文主要介紹該項目中採用的RxJava
、Retrofit
兩種技術,這二者在Android開發者中非常流行,不僅能夠優美地處理異步回調
,而且能提高代碼的性能和穩定性
。而Meizhi Android中較好的覆蓋了二者的多種應用場景,能夠給多數開發者一個全面的學習。
下面本人會對原項目的代碼進行詳細的介紹
,同時爲了讀者看的清楚其中的邏輯關係,可能會做一定調整以幫助讀者理解,比如把lambda表達式還原成普通java函數形式,以避免很多讀者對lambda並不熟悉。
##二、原項目分析
###0. clone項目到本地
第一步當然是把項目clone下來,編譯,運行。有興趣的同學可以執行這一步。
###1. 添加Stetho
抓包工具
首先,由於我們要分析retrofit,所以爲了查看app的網絡請求,有興趣的同學可以手動在代碼裏添加Stetho。Stetho
是Facebook推出的一款黑科技,能夠在chrome裏輕鬆查看app所有的網絡請求,比起iOS需要裝個Charles查看http請求方便多咯。
###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 getMeizhiList(@Path(“page”) int page);
@GET("/data/休息視頻/{page}") Observable 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);
###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 Observable進行合併
我們需要把這兩個數據源的數據拼接起來,所以我們可以考慮使用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) {//省略...}
#####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>數據源,傳給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
PS:本文原創發佈於微信公衆號「wingjay」,回覆關鍵字「程序員」獲取一份 15 本程序員經典電子書。