1. 程式人生 > >安卓開發進階之RxJava在實際專案中使用--第二篇

安卓開發進階之RxJava在實際專案中使用--第二篇

關於RxJava原理分析,請參考仍物線寫的文章—-給 Android 開發者的 RxJava 詳解。本文不對原理作過多的分析,從最快上手的角度,讓開發者使用起來,當我們有實踐經驗後回過頭來看原理分析會更清晰。
本系列共有三篇文章,分別關於Rxjava的基礎使用(最快,最實用),Retrofit使用(Github上star達22k+,安卓領域排名第一),最後是RxCache快取(大部分app都支援離線檢視功能)。

這是第二篇,網路請求框架Retrofit的使用,包括封裝,日誌列印,新增頭部資訊,Get和Post。本來想把RxCache作為第三篇文章,但是想想沒必要,就作為第二篇的一部分,所以這個系列暫時就只有這兩篇文章。

首先新增引用

        compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'

第三個將伺服器返回的資料用Gson轉換,第五個是日誌攔截器,用來列印日誌。
因為Retrofit是基於Okhttp的,所以先獲取OkHttp,

private   OkHttpClient getClient() {
        HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                Log.d("resHttp"
,message);//列印伺服器返回的內容 } }); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//BODY表示顯示伺服器返回的內容體,還有其他級別比如Header OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request().newBuilder().cacheControl(CacheControl.FORCE_NETWORK) .addHeader("X-Requested-With", "XMLHttpRequest")//新增頭部資訊,比如session // .addHeader("Cookie", "JSESSIONID="+ ZxlVar.mySession) .build(); return chain.proceed(request);}}) .addInterceptor(loggingInterceptor).build(); return httpClient; }

然後通過單例模式獲取Retrofit物件,其中傳入上面的httpClient

public  Retrofit getRetrofit(String hostUrl)
    {
        if (mRetrofit==null)
        mRetrofit=new Retrofit.Builder().baseUrl(hostUrl).client(getClient())        .addConverterFactory(GsonConverterFactory.create())//使用Gson來解析資料,這個可以自定義,但是一般不需要             
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build();
        return mRetrofit;
    }

使用之前,先新建類用來存放所有的呼叫介面,並指明呼叫方式(Get/Post),宣告引數型別及個數,以及要解析的格式,比如

@GET("/jt/getApkVersion.htm")
    Observable<ApkUpdateEntity> getApkVersson(@Query("apkCode") String apkCode);

其對應的完整URL為:

http://app.xxx.com00/jt/getApkVersion.htm?apkCode=OperationTeminal

Post請求格式如下:

@POST("/jt/icCardManager/uploadIcCardRecord")
    Observable<CardUpResEntity> upLoadCardData(@Query("terminalCode") String terminalCode
            , @Query("icCardData") String data);

其對應的完整URL為:

http://app.xxx.com00/jt/icCardManager/uploadIcCardRecord?terminalCode=xxx&icCardData=xxx

伺服器返回資料後,解析成ApkUpdateEntity格式,然後就可以呼叫裡面的內容。涉及到私密性,這裡使用網上公用介面
https://api.github.com/
不需要傳引數,呼叫程式碼如下

MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("https://api.github.com/")//一定以  /  結尾
                .create(MyApiProvider.class);        myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子執行緒進行耗時處理                .observeOn(AndroidSchedulers.mainThread())//指定在UI執行緒更新UI
        .subscribe(new Consumer<TestEntity>() {//指定按照TestEntity解析
            @Override
            public void accept(TestEntity testEntity) throws Exception {//TestEntity,這裡是解析後的結果
                //處理返回的結果                Toast.makeText(MainActivity.this,testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
            }
        });

介面宣告為:

@GET("/")
    Observable<TestEntity> getApkVersson();

如果將@GET(“/”) 換成@GET(“”)會報錯
Missing either @GET URL or @Url parameter.
執行效果
這裡寫圖片描述
至此就可以從伺服器拿到相應的資料並做處理了。我們再看另外一個重要的話題–快取。快取至少有三個好處,一是減少網路請求次數,以降低對伺服器的壓力,二是縮短網路請求時間,從本地取資料肯定比從伺服器取資料要快,三是網路情況差甚至斷網後還可以檢視資料,你現在還能看到斷網了不能使用的app嗎,很少了。既然快取這麼重要,那麼一般是怎麼做的呢?現在一般使用二級快取,即記憶體快取和硬碟快取。記憶體沒有資料,就去硬碟取,還沒有,就從伺服器取資料。注意:記憶體快取會造成堆記憶體洩露,所有一級快取通常要嚴格控制快取的大小,一般控制在系統記憶體的1/4。這兩種快取方式分別對應ASimpleCache和DiskLruCache。大家可以去網上搜索,實現起來也不復雜,但是剛上手的人可能也要花不少時間。這都不重要,我要說的是,有了Retrofit支援的RxCache,前面那些東西都不要了,對,Retrofit已經幫我們實現了二級快取,不用費老大的勁去自己實現,效果還不一定有人家的好。下面進入主題使用RxCache+Retrofit+RxJava實現二級快取。
RxCache地址

先新增依賴

    compile "com.github.VictorAlbertos.RxCache:runtime:1.8.0-2.x"
    compile 'com.github.VictorAlbertos.Jolyglot:gson:0.0.3'

然後獲取RxCache物件

public CacheProvider getCacheProvider()
    {
        if (mCacheProvider==null) {
            mCacheProvider = new RxCache.Builder().persistence(MyRetrofitUtil.getCacheDir(getApplicationContext()), new GsonSpeaker()).using(CacheProvider.class);
        }
        return mCacheProvider;
    }

其中CacheProvider類用於存放需要快取的介面,

public interface CacheProvider {
    @Expirable(false)//false表示記憶體不足系統回收時永遠不回收
    @LifeCache(duration = 60,timeUnit = TimeUnit.MINUTES)//60分鐘有效時間
    Observable<TestEntity> getApkVersson(Observable<TestEntity> obs);
}

注意CacheProvider中的介面名getApkVersson要與MyApiProvider 中的一致。
呼叫:

MyApiProvider iGetData= MyApplication.getInstance().getRetrofit("https://api.github.com/").create(MyApiProvider.class);
        CacheProvider cache=MyApplication.getInstance().getCacheProvider();//獲取CacheProvider物件
        Observable<TestEntity> observable;
        observable= cache.getApkVersson( iGetData.getApkVersson());//呼叫介面(帶快取功能)
        observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<TestEntity>() {
            @Override
            public void accept(@NonNull TestEntity testEntity) throws Exception {                Toast.makeText(MainActivity.this,"RxCache:"+testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
            }
        });

執行效果
這裡寫圖片描述
從演示中可以看到,關閉網路後依然可以拿到快取資料展示。至此快取功能已經實現,還有兩個引數需要說明一下,

Observable<TestEntity> getApkVersson2(Observable<TestEntity> obs,EvictProvider evictProvider, DynamicKey dynamicKey);
//EvictProvider表示是否強制重新整理,下拉重新整理時就需要強制重新整理,DynamicKey表示要快取第幾頁資料,當不傳此引數時預設快取第一頁

傳值方式為 new EvictProvider(true/false),new DynamicKey(page)
還有一點就是為什麼使用Gson來解析,因為有GsonFormate工具。使用非常方便, 開發必備,還沒使用的趕緊去上手。操作效果如下:
這裡寫圖片描述

當然,上面介紹的內容不復雜,但很實用,還有很多額外的東西沒有介紹,我之所以沒有介紹,是因為我專案中沒有用到,專案中用到的知識點都介紹了。我想,如果我不加區分的把所有東西都介紹一遍,不僅文章會顯得又臭又長,還讓人抓不住重點,那樣做又與官方的API文件有什麼分別呢?這也是很多人寫部落格的一個通病,其實我也有,但我會有意識的避免。

過程中遇到的問題1:在沒有網的情況下點選第一個按鈕會報錯並且崩潰

resHttp: <-- HTTP FAILED: java.net.UnknownHostException: Unable to resolve host "api.github.com": No address associated with hostname
io.reactivex.exceptions.OnErrorNotImplementedException: Unable to resolve host "api.github.com": No address associated with hostname```

而且配置檔案已經配置網路許可權<uses-permission android:name="android.permission.INTERNET" />,上網搜了一下也沒解決,不是說沒有配置許可權,就是說模擬器有問題,重啟模擬器,還有的說是伺服器有問題,還是隻能靠自己。仔細看錯誤內容,其中OnErrorNotImplementedException提示說該異常主要是指OnError方法沒有實現,OnError方法是哪裡的呢。依稀記得重寫觀察者Observer時要重寫OnNext(),OnComplete(),OnError()等方法,對,就是這個OnError()。實現它就不報錯了。

MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("https://api.github.com/")//一定以  /  結尾
                .create(MyApiProvider.class);        myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子執行緒進行耗時處理                .observeOn(AndroidSchedulers.mainThread())//指定在UI執行緒更新UI
        .subscribe(new Consumer<TestEntity>() {//指定按照TestEntity解析
            @Override
            public void accept(TestEntity testEntity) throws Exception {//TestEntity,這裡是解析後的結果
                //處理返回的結果                Toast.makeText(MainActivity.this,testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
            }
        });

修改後的程式碼為

MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("http://api.github.com/")//一定以  /  結尾
                .create(MyApiProvider.class);
        myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子執行緒進行耗時處理
                .observeOn(AndroidSchedulers.mainThread())//指定在UI執行緒更新UI
        .subscribe(new Observer<TestEntity>() {//new Observer()得到觀察者,並訂閱被觀察者,呼叫時順序相反
            @Override
            public void onSubscribe(Disposable d) {
            }
            @Override
            public void onNext(TestEntity testEntity) {
                Toast.makeText(MainActivity.this,"NoCache:"+testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
            }
            @Override
            public void onError(Throwable e) {
                Toast.makeText(MainActivity.this,"NoCacheError:"+e.toString(),Toast.LENGTH_LONG).show();
            }
            @Override
            public void onComplete() {
            }
        });

也就是將new Consumer<TestEntity>() 後面的內容替換掉,不能偷懶只重寫OnNext()方法,如果要考慮異常的話,還有重寫OnError()方法。

過程中遇到的問題2:在沒有網的情況下,第一次啟動app,先點選第二個按鈕會報錯並且崩潰,如果點選了第一個按鈕,再點選第二個按鈕則沒問題。
其實這與問題1是同一個問題但是表現不一樣,沒有快取時點選快取按鈕則去網路請求,此時沒有網,就出現問題1的場景,而先點選按鈕1後,資料被快取了,再點選快取按鈕就去取快取沒有去網路請求,所以不報錯。嗯,就是這樣。

修改後的執行效果
這裡寫圖片描述