關於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後,數據被緩存了,再點擊緩存按鈕就去取緩存沒有去網絡請求,所以不報錯。嗯,就是這樣。
修改後的運行效果