最近,我嘗試使用RxJava開發了一款閑時備份app。我必須承認,一旦你get到了正確的方式,RxJava幾乎感覺就像作弊。一切看起來更簡潔,多個請求能夠被組合,且非常容易控制。通過在UI線程觀察和在其他線程訂閱的方式,能夠通過嚴格模式的檢測,而且,你能了解到所有最酷的好東西就是在Android上使用RxJava。我不能夠很容易發現的是,如何儲存我的請求的結果,確保即使沒有網絡連接時,能夠為用戶呈現緩存的內容,同時還是使用Reactive的方式處理一切事情。
緩存vs未緩存
直接從Rest獲取結果顯示在UI上在很多情況下是合適的,比如當要顯示一個參數不可預測的搜索結果的時候(想想Ebay,或者亞馬遜,用戶每次查找的東西都是不一樣的)。
可是有一些情況,顯示之前獲取到的結果可以顯著地提高用戶體驗(相比於顯示加載進度條或者空白頁面)。這種情況包括你的Twitter訂閱,一個剛剛在5分鍾之前獲取過數據的本地天氣預報,或者一個指定用戶的github倉庫列表。
這里你可以看到,一個相同的activity使用緩存的版本和不使用緩存的版本之間的區別:
出於這個原因,我試圖找出一個簡潔地方式來緩存請求的結果,同時保持使用Reactive方式的流程。
存儲器是真理的唯一來源
全部都是reactive
如果我們想要緩存數據同時保持在相同的subscription中一切不變,事情變得有點凌亂。請求的結果拋給UI線程,並且響應結果也被儲存在存儲器(storage)中。UI也訂閱了從存儲器(storage)獲取數據,它會檢查哪個結果先返回,返回的數據是否過時。
緩存
在這個混合使用的情況中,UI僅訂閱存儲器(storage)的數據,並且使用一個外觀類類封裝了存儲器和向存儲器中填充數據的retrofit客戶端的subscription。一旦存儲器中被填充了新數據,UI線程將會自動地收到所有改動的通知。
在這種情況下,observable作為一個hot observable,在它被訂閱的第一時間,它發出存儲器中的內容,和其他任何它可能會發生的改變。
口說無憑,讓我們來看下代碼
下面這些代碼的一個可以運行的示例可以在我的github倉庫找到。為了寫這個例子,我從看起來驅動了99% rest相關示例程序的被濫用的Github api開始。先對Github說聲抱歉。
首先得有一個存儲器。 我封裝了一個 SQLite幫助類(這是我用手頭的腳本生成的),它包含了一個PublishSubject。當插入(insert)方法被調用時,PublicSubject能夠收到訂閱,並且我們會收到通知。
public class ObservableRepoDb { private PublishSubject<List<Repo>> mSubject = PublishSubject.create(); private RepoDbHelper mDbHelper; private List<Repo> getAllReposFromDb() { List<Repo> repos = new ArrayList<>(); // .. performs the query and fills the result return repos; } public Observable<List<Repo>> getObservable() { Observable<List<Repo>> firstTimeObservable = Observable.fromCallable(this::getAllReposFromDb); return firstTimeObservable.concatWith(mSubject); } public void insertRepo(Repo r) { // ... // performs the insertion on the SQLite helper // ... List<Repo> result = getAllReposFromDb(); mSubject.onNext(result); } }
我們現在已經得到拼圖的第一塊:一個能夠被訂閱的存儲器(storage)。使用concat操作是因為我們想在它一被訂閱就將存儲的內容發出去。
接下來是外觀類,在這里我們能夠得到我們訂閱的數據,且我們能夠開始一個新的更新操作。
public class ObservableGithubRepos { ObservableRepoDb mDatabase; private BehaviorSubject<String> mRestSubject; // ... public Observable<List<Repo>> getDbObservable() { return mDatabase.getObservable(); } public void updateRepo(String userName) { Observable<List<Repo>> observable = mClient.getRepos(userName); observable.subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(l -> mDatabase.insertRepoList(l)); } }
需要注意的是一切都是從UI線程發生的。這是因為我們打算將訂閱到數據庫的observable作為唯一的數據源。
現在,假設observable現在是hot,我們不能為了停止我們可能放在那里的任意進度指示器而監聽聽其的onComplete方法。我們需要的是另一個subject,讓我們必定能夠更新請求,所以下面是新的外觀類:
public class ObservableGithubRepos { // ... public Observable<List<Repo>> getDbObservable() { return mDatabase.getObservable(); } public Observable<String> updateRepo(String userName) { BehaviorSubject<String> requestSubject = BehaviorSubject.create(); Observable<List<Repo>> observable = mClient.getRepos(userName); observable.subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(l -> { mDatabase.insertRepoList(l); requestSubject.onNext(userName);}, e -> requestSubject.onError(e), () -> requestSubject.onCompleted()); return requestSubject; } }
在UI端(activity或者fragment)我們必須訂閱存儲器來獲取數據,同時也得訂閱請求的observable以停止進度指示器。每次一個更新被請求的時候,發出掛起請求的狀態的一個observable就會被返回。
mObservable = mRepo.getDbObservable(); mProgressObservable = mRepo.getProgressObservable() mObservable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe(l -> { mAdapter.updateData(l); }); Observable<List<Repo>> progressObservable = mRepo.updateRepo("fedepaol"); progressObservable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(s -> {}, e -> { Log.d("RX", "There has been an error"); mSwipeLayout.setRefreshing(false); }, () -> mSwipeLayout.setRefreshing(false));
請記住DbObservable
是一個hot的,所以每次調用updateRepo
的時候,數據庫將會被查詢結果填充,並且UI接下來將收到通知。
SqlBrite
如果你覺得所有這些封裝看起來是非常費力的,來自Square的多產的伙計寫了一個SqlBrite,它是一個為了和這個相同的目的而編寫的超級通用的數據庫封裝。我保證它更好用,並且比我們自己寫的個人版本更經得起考驗。
結論
我不知道加入這是否是一個使用RxJava的良好的方式。也許我結束這個場景只是因為我對於RxJava沒有100%的信心,而且我在中間加入了一些非Rx的東西以便更好地控制它。由於我們能夠修改從http客戶端填充存儲器的流程,或者從存儲器本身發出的流程。
在任何情況下,擁有一個真理之源將會看起來更加清晰,並且我覺得使用這種方式來處理像預下載、計划更新以便給用戶呈現最新的數據將更加容易。