RxJava 系列番外篇:一個 RxJava 解決複雜業務邏輯的案例

張磊BARON發表於2017-01-04

之前寫過一系列RxJava1的文章,也承諾過會盡快有RxJava2的介紹。無奈實際專案中還未真正的使用RxJava2,不敢妄動筆墨。所以這次還是給大家分享一個使用RxJava1解決問題的案例,希望對大家在使用RxJava的時候有一點點啟發。對RxJava還不瞭解的同學可以先去看看我之前的RxJava系列文章:

業務場景

MinimalistWeather這個開源的天氣App來舉例:

進入App首頁後,首先我們需要從資料庫中獲取當前城市的天氣資料,如果資料庫中存在天氣資料則在UI頁面上展示天氣資料;如果資料庫中未儲存當前城市的天氣資料,或者已儲存的天氣資料的釋出時間相比現在已經超過了一小時,並且網路屬於連線狀態則呼叫API從服務端獲取天氣資料。如果獲取到到的天氣資料釋出時間和當前資料庫中的天氣資料釋出時間一致則丟棄掉從服務端獲取到的天氣資料,如果不一致則更新資料庫並且在頁面上展示最新的天氣資訊。(同時天氣資料來源是可配置的,可選擇是小米天氣資料來源還是Know天氣資料來源)

解決方案

首先我們需要建立一個從資料庫獲取天氣資料的Observable observableForGetWeatherFromDB,同時我們也需要建立一個從API獲取天氣資料的Observable observableForGetWeatherFromNetWork;為了在無網路狀態下免於建立observableForGetWeatherFromNetWork我們在這之前需要首先判斷下網路狀態。最後使用contact操作符將兩個Observable合併,同時使用distincttakeUntil操作符來過濾篩選資料以符合業務需求,然後結合subscribeOnobserveOn做執行緒切換。上述這一套複雜的業務邏輯如果使用傳統編碼方式將是極其複雜的。下面我們來看看使用RxJava如何清晰簡潔的來實現這個複雜的業務:

Observable<Weather> observableForGetWeatherData;
//首先建立一個從資料庫獲取天氣資料的Observable
Observable<Weather> observableForGetWeatherFromDB = Observable.create(new Observable.OnSubscribe<Weather>() {
    @Override
    public void call(Subscriber<? super Weather> subscriber) {
        try {
            Weather weather = weatherDao.queryWeather(cityId);
            subscriber.onNext(weather);
            subscriber.onCompleted();
        } catch (SQLException e) {
            throw Exceptions.propagate(e);
        }
    }
});

if (!NetworkUtils.isNetworkConnected(context)) {
    observableForGetWeatherData = observableForGetWeatherFromDB;
} else {
    //接著建立一個從網路獲取天氣資料的Observable
    Observable<Weather> observableForGetWeatherFromNetWork = null;
    switch (configuration.getDataSourceType()) {
        case ApiConstants.WEATHER_DATA_SOURCE_TYPE_KNOW:
            observableForGetWeatherFromNetWork = ApiClient.weatherService.getKnowWeather(cityId)
                    .map(new Func1<KnowWeather, Weather>() {
                        @Override
                        public Weather call(KnowWeather knowWeather) {
                            return new KnowWeatherAdapter(knowWeather).getWeather();
                        }
                    });
            break;
        case ApiConstants.WEATHER_DATA_SOURCE_TYPE_MI:
            observableForGetWeatherFromNetWork = ApiClient.weatherService.getMiWeather(cityId)
                    .map(new Func1<MiWeather, Weather>() {
                        @Override
                        public Weather call(MiWeather miWeather) {
                            return new MiWeatherAdapter(miWeather).getWeather();
                        }
                    });
            break;
    }
    assert observableForGetWeatherFromNetWork != null;
    observableForGetWeatherFromNetWork = observableForGetWeatherFromNetWork
            .doOnNext(new Action1<Weather>() {
                @Override
                public void call(Weather weather) {
                    Schedulers.io().createWorker().schedule(() -> {
                        try {
                            weatherDao.insertOrUpdateWeather(weather);
                        } catch (SQLException e) {
                            throw Exceptions.propagate(e);
                        }
                    });
                }
            });

    //使用concat操作符將兩個Observable合併
    observableForGetWeatherData = Observable.concat(observableForGetWeatherFromDB, observableForGetWeatherFromNetWork)
            .filter(new Func1<Weather, Boolean>() {
                @Override
                public Boolean call(Weather weather) {
                    return weather != null && !TextUtils.isEmpty(weather.getCityId());
                }
            })
            .distinct(new Func1<Weather, Long>() {
                @Override
                public Long call(Weather weather) {
                    return weather.getRealTime().getTime();//如果天氣資料釋出時間一致,我們再認為是相同的資料從丟棄掉
                }
            })
            .takeUntil(new Func1<Weather, Boolean>() {
                @Override
                public Boolean call(Weather weather) {
                    return System.currentTimeMillis() - weather.getRealTime().getTime() <= 60 * 60 * 1000;//如果天氣資料釋出的時間和當前時間差在一小時以內則終止事件流
                }
            });
}

observableForGetWeatherData.subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Weather>() {
            @Override
            public void call(Weather weather) {
                displayWeatherInformation();
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable throwable) {
                Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_LONG).show();
            }
        });複製程式碼

上面的程式碼看起來比較複雜,我們採用Lambda表示式簡化下程式碼:

Observable<Weather> observableForGetWeatherData;
//首先建立一個從資料庫獲取天氣資料的Observable
Observable<Weather> observableForGetWeatherFromDB = Observable.create(new Observable.OnSubscribe<Weather>() {
    @Override
    public void call(Subscriber<? super Weather> subscriber) {
        try {
            Weather weather = weatherDao.queryWeather(cityId);
            subscriber.onNext(weather);
            subscriber.onCompleted();
        } catch (SQLException e) {
            throw Exceptions.propagate(e);
        }
    }
});

if (!NetworkUtils.isNetworkConnected(context)) {
    observableForGetWeatherData = observableForGetWeatherFromDB;
} else {
    //接著建立一個從網路獲取天氣資料的Observable
    Observable<Weather> observableForGetWeatherFromNetWork = null;
    switch (configuration.getDataSourceType()) {
        case ApiConstants.WEATHER_DATA_SOURCE_TYPE_KNOW:
            observableForGetWeatherFromNetWork = ApiClient.weatherService.getKnowWeather(cityId)
                    .map(knowWeather -> new KnowWeatherAdapter(knowWeather).getWeather());
            break;
        case ApiConstants.WEATHER_DATA_SOURCE_TYPE_MI:
            observableForGetWeatherFromNetWork = ApiClient.weatherService.getMiWeather(cityId)
                    .map(miWeather -> new MiWeatherAdapter(miWeather).getWeather());
            break;
    }
    assert observableForGetWeatherFromNetWork != null;
    observableForGetWeatherFromNetWork = observableForGetWeatherFromNetWork
            .doOnNext(weather -> Schedulers.io().createWorker().schedule(() -> {
                try {
                    weatherDao.insertOrUpdateWeather(weather);
                } catch (SQLException e) {
                    throw Exceptions.propagate(e);
                }
            }));

    //使用concat操作符將兩個Observable合併
    observableForGetWeatherData = Observable.concat(observableForGetWeatherFromDB, observableForGetWeatherFromNetWork)
            .filter(weather -> weather != null && !TextUtils.isEmpty(weather.getCityId()))
            .distinct(weather -> weather.getRealTime().getTime())//如果天氣資料釋出時間一致,我們再認為是相同的資料從丟棄掉
            .takeUntil(weather -> System.currentTimeMillis() - weather.getRealTime().getTime() <= 60 * 60 * 1000);//如果天氣資料釋出的時間和當前時間差在一小時以內則終止事件流
}

observableForGetWeatherData.subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(weather -> displayWeatherInformation(),
                throwable -> Toast.makeText(context, throwable.getMessage(), Toast.LENGTH_LONG).show());複製程式碼

小技巧

在上述的實現中有幾點是我們需要注意的:

  1. 為什麼我需要在判斷網路那塊整個if else?這樣看起來很不優雅,我們通過RxJava符完全可以實現同樣的操作啊!之所以這樣做是為了在無網路狀況下去建立不必要的Observable observableForGetWeatherFromNetWork;

  2. 更新資料庫的操作不應該阻塞更新UI,因此我們在observableForGetWeatherFromNetWorkdoOnNext中需要通過Schedulers.io().createWorker()去另起一條執行緒,以此保證更新資料庫不會阻塞更新UI的操作。

    有同學可能會問為什麼不在doOnNext之後再呼叫一次observeOn把更新資料庫的操作切換到一條新的子執行緒去操作呢?其實一開始我也是這樣做的,後來想想不對。整個Observable的事件傳遞處理就像是在一條流水線上完成的,雖然我們可以通過observeOn來指定子執行緒去處理更新資料庫的操作,但是隻有等這條子執行緒完成了更新資料庫的任務後事件才會繼續往後傳遞,這樣就阻塞了更新UI的操作。對此有疑問的同學可以去看看我之前關於RxJava原始碼分析的文章或者自己動手debug看看。

問題

最後給大家留個兩個問題:

  1. 上述程式碼是最佳實現方案嗎?還有什麼更加合理的做法?
  2. 我們在observableForGetWeatherData中使用distincttakeUntil過濾篩選天氣資料的時候網路請求會不會已經發出去了?這樣做還有意義嗎?

歡迎大家留言討論。

本文中的程式碼在MinimalistWeather中的WeatherDataRepository類中有同樣的實現,文章中為了更完整的將整個實現過程呈現出來,對程式碼做了部分改動。

如果大家喜歡這一系列的文章,歡迎關注我的知乎專欄、Github以及簡書。

相關文章