帶你學開源項目:Meizhi Android之RxJava & Retrofit最佳實踐

rh2172 8年前發布 | 25K 次閱讀 Retrofit RxJava 開源 Android開發 移動開發

 

零、背景

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

《帶你學開源項目》系列將帶領你深入閱讀及分析當前流行的一些開源項目,并針對其中采用的新技術與精妙之處進行細致的闡述,以期讓你快速掌握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

來自: http://www.jianshu.com/p/47e72693a302

 本文由用戶 rh2172 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!