開源項目解析:Meizhi Android之RxJava & Retrofit最佳實踐

如果你對開源項目分析感興趣,歡迎加入我們的android-open-source-project-cracking
原創地址:http://www.jianshu.com/p/47e72693a302

零、背景

比起閱讀枯燥的技術文檔,獨自苦苦摸索新技術的基本用法,還有一種更好更快速也更有效的提高自身技術的方法,那就是閱讀學習優質的開源項目,通過仿寫、練習最終達到理解,潛移默化提升自身編程技能。

《帶你學開源項目》系列將帶領你深入閱讀及分析當前流行的一些開源項目,並針對其中採用的新技術與精妙之處進行細緻的闡述,以期讓你快速掌握Android開發中的多種強大技能點。

一、本期開源項目Meizhi Android

本次的開源項目選擇了Meizhi Android,本文主要介紹該項目中採用的RxJava、Retrofit兩種技術,這二者在Android開發者中非常流行,不僅能夠優美地處理異步回調,而且能提高代碼的性能和穩定性。而Meizhi Android中較好的覆蓋了二者的多種應用場景,能夠給多數開發者一個全面的學習。

下面本人會對原項目的代碼進行詳細的介紹,同時爲了讀者看的清楚其中的邏輯關係,可能會做一定調整以幫助讀者理解,比如把lambda表達式還原成普通java函數形式,以避免很多讀者對lambda並不熟悉。

二、原項目分析

  1. clone項目到本地
    第一步當然是把項目clone下來,編譯,運行。有興趣的同學可以執行這一步。

  2. 添加Stetho抓包工具
    首先,由於我們要分析retrofit,所以爲了查看app的網絡請求,有興趣的同學可以手動在代碼裏添加Stetho。Stetho是Facebook推出的一款黑科技,能夠在chrome裏輕鬆查看app所有的網絡請求,比起iOS需要裝個Charles查看http請求方便多咯。
    Stetho使用場景

  3. 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格式結果,方便RxJava進行進一步處理。

@GET("/data/福利/{page}") Observable<MeizhiList> getMeizhiList(@Path("page") int page);
@GET("/data/休息視頻/{page}") Observable<GankVideoList> getGankVideoList(@Path("page") int page);
  • 1
  • 2

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

iii. DrakeetFactory: 這個類用來對外生成單例GankApi實例,爲確保GankApi實例只生成一次。

public static GankApi getGankApi() {    
    synchronized (monitor) {        
       if (sGankApi == null) {            
          sGankApi = new DrakeetRetrofit().getGankApi();        
       }       
       return sGankApi;    
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

所以,在實際應用場景中,比如我們想要發起一個http請求來獲取福利數據,那麼我們可以採用以下方式:

GankApi gankApi = DrakeetFactory.getGankApi();
Observable<MeizhiList> meizhiList = gankApi. getMeizhiList(10);
  • 1
  • 2

這裏寫圖片描述

  1. 首頁的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

ii. zip: 將兩個retrofit接口請求後得到的兩個數據源Observable Observable進行合併
我們需要把這兩個數據源的數據拼接起來,所以我們可以考慮使用zip操作符,該操作符可以將兩個數據源發射出來的數據依次組裝在一起。

比如一個Observable數據源依次發射出1, 3, 5, 7, 另一個Observable數據源依次發射出a, b, c, d,那麼zip操作符組裝後會對外發射出1a, 3b, 5c, 7d這樣的數據。

而我們需要的正是這樣。

Observable一次對外發射一個MeizhiList對象,Observable一次對外發射一個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)
  • 1
  • 2
  • 3
  • 4

其中mergeVideoWithMeizhi是一個合併函數,把video信息與meizhi信息合併成新的MeizhiWithVideo對象。

public MeizhiWithVideoList
mergeVideoWithMeizhi(MeizhiList meizhiList, VideoList videoList) {//省略...}
  • 1
  • 2

這裏寫圖片描述

iii. 對MeizhiWithVideo對象進行排序。
在上面,我們通過合併,得到了 Observable數據源,這個數據源對外發射出一個MeizhiWithVideoList對象,這個對象裏有10個MeizhiWithVideo數據,我們可以對這10個數據利用它們的發佈日期進行排序。

所以我們要實現以下幾步:

  • 先把Observable<MeizhiWithVideoList>數據源轉化爲Observable< List<
    MeizhiWithVideo>>,從對外發一個MeizhiWithVideoList對象變成對外發射一個List< MeizhiWithVideo>對象;
  • 再把Observable< 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());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 利用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();    
      }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在實際的Activity中的網絡請求:

public class MyActivity extends BaseActivity {

    private void loadData() {
        Subscription s = gankApi.getMeizhiList(10)                           
                                     .subscribeOn(Schedulers.io())
                                     .observeOn(AndroidSchedulers.mainThread())
                                     .subscribe(...);
        addSubscription(s);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
三、改進及總結
  • 1

本文通過對開源項目Meizhi Android進行分析,瞭解了Retrofit,RxJava的實際應用場景,也對於二者有了更加深入的認識。

不過本人認爲該項目還有一些可以改善的地方,比如Retrofit中利用DrakeetFactory工廠來生成GankApi的單例,但是new DrakeetRetrofit().getGankApi();也是一個可以生成GankApi的方法,而且是public的,那麼如果新的開發者忘記調用DrakeetFactory來生成GankApi的實例,而是採用後者,那麼工廠模式就達不到預期的目的了。我認爲可以把new DrakeetRetrofit().getGankApi();這個操作內容放在DrakeetFactory工廠內部,並且設置爲private屬性,這樣的話如果想要獲得GankApi實例,就必須依靠DrakeetFactory來生成,從而真正保證了單例的優勢。

(function () { ('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;var numbering = $('
    ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append( numbering); for (i = 1; i
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章