一、應用背景
今天,我們以一個請求天氣資料的例子,來演示如何用RxJava
實現網路重連時的自動請求,首先,我們對這個需求進行一個簡單的描述,整個專案的框架如下所示:
- 在應用啟動時,我們會啟動定位模組,該定位模組在後臺每隔一段時間發起一次定位請求,拿到定位的結果後,我們通過該城市向伺服器發起請求,以獲取對應城市的天氣資訊進行展示。
- 但是在拿到城市之後向伺服器請求天氣的過程中有可能是處於沒有網路的狀態,導致無法獲取城市的天氣資訊並重新整理介面,因此,我們需要檢測網路的狀態,在網路重連的時候,讀取上一次快取的城市,向伺服器發起請求以獲取城市對應天氣資訊。
本文的示例程式碼在 RxSample 的第十一章中。
二、示例
2.1 定位模組
我們通過一個後臺執行緒來模擬定位的過程,它每隔一段時間獲取一次定位的結果,並將該結果通過mCityPublish
傳送資料給它的訂閱者。
//用於釋出定位到的城市結果。
private PublishSubject<Long> mCityPublish;
//模擬定位模組的回撥。
private void startUpdateLocation() {
mLocationThread = new Thread() {
@Override
public void run() {
while (true) {
try {
for (long cityId : CITY_ARRAY) {
if (isInterrupted()) {
break;
}
Log.d(TAG, "重新定位");
Thread.sleep(5000);
Log.d(TAG, "定位到城市資訊=" + cityId);
mCityPublish.onNext(cityId);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
mLocationThread.start();
}
複製程式碼
在mCityPublish
傳送訊息到訂閱者收到訊息之間,我們還需要做一些特殊的處理:
private Observable<Long> getCityPublish() {
return mCityPublish.distinctUntilChanged().doOnNext(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
saveCacheCity(aLong);
}
});
}
複製程式碼
這裡我們做了兩步處理:
- 使用
distinctUntilChanged
對定位結果進行過濾,如果此次定位的結果和上次定位的結果相同,那麼不通知訂閱者。distinctUntilChanged
的原理圖如下所示: - 使用
doOnNext
,在返回結果給訂閱者之前,先把最新一次的定位結果儲存起來,用於在之後網路重連之後進行請求。
2.2 網路狀態模組
與定位模組類似,我們也需要一個mNetStatusPublish
,其型別為PublishSubject
,它在網路狀態發生變化時通知訂閱者。這裡需要註冊一個廣播,在收到廣播之後,我們通過mNetStatusPublish
通知訂閱者,程式碼如下:
private void registerBroadcast() {
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mNetStatusPublish != null) {
mNetStatusPublish.onNext(isNetworkConnected());
}
}
};
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(mReceiver, filter);
}
複製程式碼
在收到網路狀態變化的訊息之後:
private Observable<Long> getNetStatusPublish() {
return mNetStatusPublish.filter(new Predicate<Boolean>() {
@Override
public boolean test(Boolean aBoolean) throws Exception {
return aBoolean && getCacheCity() > 0;
}
}).map(new Function<Boolean, Long>() {
@Override
public Long apply(Boolean aBoolean) throws Exception {
return getCacheCity();
}
}).subscribeOn(Schedulers.io());
}
複製程式碼
這裡我們做了兩步處理:
- 使用
filter
對訊息進行過濾,只有在 聯網情況並且之前已經定位到了城市 之後才通知訂閱者,filter
的原理圖如下所示,該操作符用於過濾掉一些不需要的資料: - 使用
map
,讀取當前快取的城市名,返回給訂閱者,map
的原理圖如下所示,該操作符可以用於執行變換操作。
2.3 網路請求模組
在2.1
和2.2
中,我們分別用getCityPublish()
和getNetStatusPublish()
來獲取被訂閱者,它們分別對應於定位模組和網路狀態模組發生變化時所傳送的城市資料,下面來看我們通過城市資料獲取城市天氣資訊的程式碼:
private void startUpdateWeather() {
Observable.merge(getCityPublish(), getNetStatusPublish()).flatMap(new Function<Long, ObservableSource<WeatherEntity>>() {
@Override
public ObservableSource<WeatherEntity> apply(Long aLong) throws Exception {
Log.d(TAG, "嘗試請求天氣資訊=" + aLong);
return getWeather(aLong).subscribeOn(Schedulers.io());
}
}).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
Log.d(TAG, "請求天氣資訊過程中發生錯誤,進行重訂閱");
return Observable.just(0);
}
});
}
}).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<WeatherEntity>() {
@Override
public void onSubscribe(Disposable disposable) {
mCompositeDisposable.add(disposable);
}
@Override
public void onNext(WeatherEntity weatherEntity) {
WeatherEntity.WeatherInfo info = weatherEntity.getWeatherinfo();
if (info != null) {
Log.d(TAG, "嘗試請求天氣資訊成功");
StringBuilder builder = new StringBuilder();
builder.append("城市名:").append(info.getCity()).append("\n").append("溫度:").append(info.getTemp()).append("\n").append("風向:").append(info.getWD()).append("\n").append("風速:").append(info.getWS()).append("\n");
mTvNetworkResult.setText(builder.toString());
}
}
@Override
public void onError(Throwable throwable) {
Log.d(TAG, "嘗試請求天氣資訊失敗");
}
@Override
public void onComplete() {
Log.d(TAG, "嘗試請求天氣資訊結束");
}
});
}
private Observable<WeatherEntity> getWeather(long cityId) {
WeatherApi api = new Retrofit.Builder()
.baseUrl("http://www.weather.com.cn/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(WeatherApi.class);
return api.getWeather(cityId);
}
複製程式碼
這裡我們做了以下幾個操作:
-
使用
merge
合併兩個資料來源,我們通過getWeather(long cityId)
來獲取城市資訊,這裡面用到了 RxJava2 實戰知識梳理(4) - 結合 Retrofit 請求新聞資訊 的知識,只不過這裡的介面是使用的天氣資訊網的資料,merge
的原理在 RxJava2 實戰知識梳理(8) - 使用 publish + merge 優化先載入快取,再讀取網路資料的請求過程 也已經做了介紹。 -
使用
retryWhen
進行重訂閱,因為在獲取到城市,之後轉換成城市天氣資訊的時候有可能發生錯誤,如果發生了錯誤,那麼整個呼叫鏈就結束了,需要重新訂閱。這裡的重訂閱使用的retryWhen
操作符,關於重訂閱更詳細的解釋可以看前面的這篇文章 RxJava2 實戰知識梳理(6) - 基於錯誤型別的重試請求,下面是其中的部分說明: -
使用
observeOn
切換到主執行緒進行介面的更新,原理如: RxJava2 實戰知識梳理(1) - 後臺執行耗時操作,實時通知 UI 更新
2.4 示例演示
本章的示例程式碼在 RxSample 的第十一章中,我們演示兩種情況:
- 正常聯網情況,定位回撥的間隔為
1s
: 控制檯輸出如下,可以看到只有當前後兩次定位資訊不同時才會發起網路請求天氣資訊: - 在非聯網的時候進入,並只進行一次定位,然後在切換到有網的狀態。 此時控制檯的輸出如下,可以看到在網路重連之後,我們使用快取的城市自動重新發起了請求:
2.6 操作符
在這個示例中,我們用到了以下幾種操作符,如果有不明白的地方,大家可以去對應的連結中檢視更詳細的解釋:
更多文章,歡迎訪問我的 Android 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/