RxJava 2: 用Retrofit2架構Android MVVM 生命週期
原文: https://medium.com/@manuelvicnt/rxjava2-android-mvvm-lifecycle-app-structure-with-retrofit-2-cf903849f49e#.elz8jqnoi
一年多前,我寫了一個帖子MVVM, RxJava and Retrofit。現在看, 這個帖子有點過時了。你會驚奇,一年之內你能學習多少東西。如果你回顧一下,你會對自己的程式碼感到尷尬。不僅是程式碼本身,還有你怎麼到達那裡的過程。對我來說,全部都好像是遺留程式碼。
根據新的情景和庫, 我試著改進這個架構。讓我們繼續同一個例子(在這裡獲取更多資訊)。這次,我將使用第一個穩定版本的Rxjava2和Retrofit。
在這篇文章中,我們將理解,在用Retrofit的MVVM架構的實際例子中, 如何使用Rxjava 2。我們也將講到,利用網路請求響應到檢視層的生命週期,怎麼提高你應用的效能。
獲取資訊
應用結構
如果我們快速瞭解下不同層...
- Retrofit層: 實際上是發出網路請求
- APIService層:負責網路請求,包括解析響應,如果有必要處理它
- RequestManager層:準備將要傳送的資料;連結不同的網路請求。
- ViewModel層: 處理檢視層需要的邏輯
- View層:檢視是啞的,只是處理使用者輸入
親自動手
在這篇文章中,我將大量論述一個小專案,你將看見一切是怎麼實現的
生命週期導致Views 和 ViewModels之間的問題?
在上個用Rxjava1的文章中,我們在ViewModels中有Subjects,響應資訊到有Subscribers的Views中。當我說我在一年之中我學習了很多,你記得這部分?嗯,就是這個例子。
我們全都遇見過相同的問題:如果應用回到後臺,我們不想取消網路請求,或者多次請求網路。
其中我們面對的一個問題是,Subscriber/Observer的onNext() 或者onComplete()方法被呼叫,但View不在螢幕中(注:即不在前臺)。如果Subscriber試著回信息到檢視(通過一個BusManager或者一個Callback),在那個方法中,我們試著更新任何UI的控制元件,那麼我們的應用可能Crash
如果你看一下新的程式碼倉庫,Views 和 ViewModels之間的通訊是通過一個介面(或者叫回調),我們叫它Contract。這給你提供了靈活性:在ViewModel的上面即插即用任何的View。
假設你有不同的Views,取決於你的裝置是智慧手機、平板電腦或者智慧手錶,所有這些都可能分享同一個ViewModel,但是反之不亦然(注:一個View不能有多個ViewModel)。
怎麼解決生命週期的問題?
定義一個介面來每個時刻發生了什麼
public interface Lifecycle {
interface View {
}
interface ViewModel {
void onViewResumed();
void onViewAttached(@NonNull Lifecycle.View viewCallback);
void onViewDetached();
}
}
View將在自己的onResume()中,呼叫Lifecycle.ViewModel#onViewResumed();在onStart()中呼叫Lifecycle.ViewModel#onViewAttached(this);在onDestroy()中呼叫Lifecycle.ViewModel#onViewDetached()。
這樣,ViewModel清楚了生命週期,什麼時候顯示什麼或者不顯示的邏輯將移到ViewModel中(本應該這樣的),所以當有資訊的時候,它能有相應的響應和通知檢視。
View和ViewModel之間的Contract
Contact定義了View需要從ViewModel獲取了什麼,反之亦然。通常,我們根據一個介面定義一個contract,儘管你也可以根據一個功能來定義。
在我們的例子中,我們有個Home介面,能夠重新整理User資料。我們定義我們的contract為:
public interface HomeContract {
interface View extends Lifecycle.View {
void showSuccessfulMessage(String message);
}
interface ViewModel extends Lifecycle.ViewModel {
void getUserData();
}
}
這個contract擴充套件了Lifecycle contract,所以ViewModel也將知道生命週期
Rxjava 2 響應流的型別
Rxjava 2中,引進了一些概念,重新命名了另外一些。看下文件獲取更多的資訊
兩者之間重要的不同是背壓的處理。基本上,Flowable是能夠處理背壓的Observer,同樣的關係連線了FlowableProcessor和Subject,Subscriber和Observer,等等。
記住,Completable、Single和Maybe不處理背壓。
為了學習的目的,我們將Retrofit返回Observable物件。如果我們想處理背壓呢?如果我們知道預期的結果,想通過指定想要獲得的Stream來優化我們的程式碼呢?
使用Completable
讓我們註冊呼叫作為例子。因為RegistrationAPIService是處理這個資訊的,我們不想返回Stream,因為在RequestManager層響應沒有使用。我們僅僅關係這個呼叫是否成功。為此,我們返回Completable物件,忽略我們從Observable獲取的元素。
public Completable register(RegistrationRequest request) {
return registrationAPI.register(request)
.doOnSubscribe(disposable -> isRequestingRegistration = true)
.doOnTerminate(() -> isRequestingRegistration = false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(this::handleRegistrationError)
.doOnNext(registrationResponse -> processRegistrationResponse(request, registrationResponse))
.ignoreElements();
}
使用Maybe
如果我們想把response返回到RequestManager層,但是因為這是個網路請求,而且我們知道我們將收到一個物件,我們可以使用Maybe(有可能,body是空的,所以當null物件時,我們使用Maybe來避免異常)
記住,用singleElement()操作子,而不是singleElement()操作子。如果你使用第二個,你獲取不到什麼,它將拋一個異常,因為它一直會嘗試獲取第一個元素,即使沒有第一個元素。
public Maybe<LoginResponse> login(LoginRequest request) {
return loginAPI.login(request.getNickname(), request.getPassword())
.doOnSubscribe(disposable -> isRequestingLogin = true)
.doOnTerminate(() -> isRequestingLogin = false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(this::handleLoginError)
.doOnNext(this::processLoginResponse)
.singleElement();
}
使用Flowable
就像我們前面說的,Flowable和Observable有相同的行為,但是能夠處理背壓。為此,當Observable轉為Flowable的時候,我們不得不指定我們想要的哪個策略。
有不同的策略:Buffer(緩衝所有onNext的值,直至下游消費它),DROP(放棄最近的onNext值如果下游不能趕上),ERROR(發出MissingBackpressureException,萬一下游不能趕上)也是Observable相同的行為,LATEST(保留最新的onNext值,重寫前面的值如果下游不能趕上)和MISSING(onNext事件沒有任何緩衝和丟棄)。
在我們的Games例子中,我們使用BUFFER策略,因為我們不想失去任何game,萬一下游不能趕上。這可能有點慢,但是所有的事件就在那裡。
public Flowable<GamesResponse> getGames(GamesRequest request) {
return gamesAPI.getGamesInformation(request.getNickname())
.doOnSubscribe(disposable -> isRequestingGames = true)
.doOnTerminate(() -> isRequestingGames = false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(this::handleAccountError)
.toFlowable(BackpressureStrategy.BUFFER);
}
使用Zip操作子同時進行不同的網路請求
如果你想同時不同的網路請求,僅當所有的網路請求都成功的時候,得到通知,這時,你應該使用Zip操作子。這非常強大!這是我喜歡的操作子之一。
#UserDataRequestManager.java
public Flowable<Object> getUserData() {
return Flowable.zip(
getAccount(),
getGames(),
this::processUserDataResult);
}
private Flowable<AccountResponse> getAccount() {
return accountAPIService.getAccount(createAccountRequest());
}
private Flowable<GamesResponse> getGames() {
return gamesAPIService.getGames(createGamesRequest());
}
連線不同的網路請求
我們看到怎麼每個網路請求返回不同型別的Stream。讓我們看看我們怎麼連線它們。計劃是,Registration請求,Login請求,然後是UserData。進行多合一。
UserData返回Flowable。然而,Login請求返回Maybe。我們必須匹配它們。
#AuthenticationRequestManager.java
private MaybeSource<Object> makeGetUserDataRequest(LoginResponse loginResponse) {
return userDataRequestManager.getUserData().singleElement();
}
如果響應是成功的,Login請求將獲取UserData。我們預計getUserDataRequestMethod返回Maybe,我們可以用flatMap()操作子連線它們。
#AuthenticationRequestManager.java
public MaybeSource<Object> login() {
return loginAPIService.login(createLoginRequest())
.flatMap(this::makeGetUserDataRequest);
}
現在如果我們想呼叫Registration,然後是Login請求,我們僅僅在Completable完成後調動Login請求。我們用andThen()操作子來完成
#AuthenticationRequestManager.java
public MaybeSource<Object> register() {
return registrationAPIService.register(createBodyForRegistration())
.andThen(makeLoginRequest());
}
private MaybeSource<Object> makeLoginRequest() {
return login();
}
觀察輸入源
如果我們看看文件,我們能發現,Observers(為Observables)和Subscribers (為Flowables)怎麼在它們的藉口中暴露一個新的方法:onSubscribe()。
Observer以Disposable來訂閱,Disposable處理或者取消這個訂閱。Subscriber以Subscription來訂閱,而且可以取消訂閱,它能請求一些項(我們能在這裡看見背壓功能)。
很多時候,我們不想重寫Observer或者Subscriber的onSubscribe方法(就像我們在Rxjava 1一樣)。為此,我們僅僅可以用DisposableObserver或者DisposableSubscriber訂閱Stream。
當你觀察一個Stream,如果你想得到Subscription或者Disposable,你不得不用subscribeWith(),而不是subscribe()。
如果你不想取消訂閱,你可以使用subscribe():
public void getUserData() {
userDataRequestManager.getUserData()
.subscribe(new HomeSubscriber());
}
如果你想取消訂閱或者dispose:
public void getUserData() {
Disposable userDataSubscription = userDataRequestManager.getUserData()
.subscribeWith(new HomeSubscriber());
userDataSubscription.dispose();
}
後臺處理和生命週期
當檢視不在前臺,為了避免通知,我們得持有這個資訊直至檢視變得可見(準備做應該做的事情),然後分派這個資訊。在我們的用例中,當應用在後臺或者檢視不可見,我們僅僅想要一個網路請求而不是多個。
方案1:用生命週期Contract方法
用生命週期的方法,我們建立了一個抽象類來處理請求狀態。我們儲存狀態和那裡發生的最後一個錯誤。
我們也可以建立不同的Observers,取決於Stream的型別。比如,Login請求是由MaybeObserver處理。
protected class MaybeNetworkObserver<T> extends DisposableMaybeObserver<T> {
@Override
public void onSuccess(T value) {
requestState = REQUEST_SUCCEEDED;
}
@Override
public void onError(Throwable e) {
lastError = e;
requestState = REQUEST_FAILED;
}
@Override
public void onComplete() {
}
}
我們可以看見,在這個情況中,onSuccess(T)是設定requestState為SUCCEEDED的方法,因為它是DisposableMaybeObserver(如果它是DisposableObserver,那麼那個應該在onComplete方法)。當進行網路請求,這個Observer是在Login的ViewModel中使用。如果我們看下這個類,他的方法定義如下:
public class LoginViewModel extends NetworkViewModel implements LoginContract.ViewModel {
public void login() {
authenticationRequestManager.login()
.subscribe(new LoginObserver());
}
}
private class LoginObserver extends MaybeNetworkObserver<Object> {
@Override
public void onSuccess(Object value) {
onLoginCompleted();
}
@Override
public void onError(Throwable e) {
onLoginError(e);
}
@Override
public void onComplete() {
}
}
onLoginError() 和 onLoginCompleted() 是定義在這個類的內部,處理好的和壞的情況。正如你看見的,這個情形中,我們可以在authenticationRequestManager Maybe Stream呼叫subscribe(),因為我們不反訂閱
當應用到後臺時這麼處理呢?我們用onViewResumed()方法:
@Override
public void onViewResumed() {
@RequestState int requestState = getRequestState();
if (requestState == REQUEST_SUCCEEDED) {
onLoginCompleted();
} else if (requestState == REQUEST_FAILED) {
onLoginError(getLastError());
}
}
當檢視恢復了,我們的狀態是REQUEST_SUCCEEDED,然後我們通知檢視,如果失敗,我們以錯誤通知。可能你注意到了,當響應來了,LoginObserver類裡面的程式碼被呼叫,如果檢視就在那裡的話,我們可以通知檢視?如果檢視不在那裡,我們需要判空來避免呼叫。看下面的程式碼:
private void onLoginCompleted() {
if (viewCallback != null) {
viewCallback.hideProgressDialog();
viewCallback.launchHomeActivity();
}
}
方案2:用Processor(支援背壓的Subject)
當我們在HomeActivity中下拉重新整理時,HomeViewModel獲取UserData。我們使用Processor,而不是使用標準的Subscriber。
這個解決方案為下拉重新整理行為而設計的。我們一直想進行那個網路請求,假使你不想進行多個網路請求,然後得到最後一個響應,這個實現有一點點不一樣。
這個例子中,我們使用AsyncProcessor,因為我們僅僅想要源傳送的最後一個資訊,這個資訊還沒有被消費,不是所有的元素。
所以,當我們下拉重新整理,我們一直getUserData()網路請求。然而,當檢視從ViewModel分離的時候,我們不想取消網路,而且當檢視恢復的時候我們處理這個資訊。
關鍵是AsyncProcessor,這個物件將訂閱UserData Flowable,然後將持有這個資訊到Subscriber請求它。
因為我們一直想進行這個網路請求,我們每次建立一個新的AsyncProcessor。然後,我們用這個物件訂閱到AsyncProcessor,我們想獲取響應,然後在本地欄位中持有它(所以,當檢視分離的時候我們能處理它)。最後,我們進行網路請求用AsyncProcessor作為AsyncProcessor。
# HomeViewModel.java
private AsyncProcessor<Object> userDataProcessor;
private Disposable userDataDisposable;
public void getUserData() {
userDataProcessor = AsyncProcessor.create();
userDataDisposable = userDataProcessor.subscribeWith(new HomeSubscriber());
userDataRequestManager.getUserData().subscribe(userDataProcessor);
}
當檢視分離的時候發生了什麼?我們取消當前的Disposable。注意到,網路請求不是被取消了因為它使用AsyncProcessor訂閱了。
@Override
public void onViewDetached() {
this.viewCallback = null;
if (isNetworkRequestMade()) {
userDataDisposable.dispose();
}
}
private boolean isNetworkRequestMade() {
return userDataDisposable != null;
}
當檢視恢復的時候,我們檢查是否我們是否已經進行了一個網路請求。如果如此,我們重新連線我們的Subscriber到AsyncProcessor。如果網路請求正在進行,當訊息來的時候,我們能得到通知。如果它已經來了,我們馬上得到通知。
@Override
public void onViewResumed() {
if (isNetworkRequestMade()) {
userDataProcessor.subscribe(new HomeSubscriber());
}
}
這個解決方案的特點是,Subscriber的程式碼永遠不會在後臺執行。因為這個,我們不需要檢查檢視的為空性。viewCallback物件永遠不會為空。
private class HomeSubscriber extends DisposableSubscriber<Object> {
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
viewCallback.showSuccessfulMessage("Refreshed");
}
@Override
public void onComplete() {
viewCallback.showSuccessfulMessage("Refreshed");
viewCallback.hideLoading();
}
}
模擬Retrofit網路請求
如果你看看這個工程,我用一個客戶端來模擬網路請求,新增延時,所以當應用回到後臺時我們能測試它。
Retrofit Builder上用RxJava2CallAdapterFactory,在Retrofit上開啟Rxjava 2特性。
public static Retrofit getAdapter() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new MockInterceptor())
.build();
return new Retrofit.Builder()
.baseUrl("http://www.mock.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
攔截器一直在兩秒後返回一個成功的響應。這是可以改進的,檢查哪個請求已經進行了,然後返回作為body的部分的正確JSON響應。
public class MockInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
addDelay();
return new Response.Builder()
.code(200)
.message("OK")
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), "{}"))
.build();
}
private void addDelay() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
其他的考量
當你看看程式碼倉庫,一些部分的程式碼相當糟糕。你看見Singletons的使用了嗎?(比如,UserDataRequestManager),這個太傷眼了但是我沒有時間把它變得更好。
你可能想知道... 問題是什麼?嗯,單元測試的singletons是最糟糕的事情,因為我們在單元測試中持有他們的狀態。
這麼修復它呢?依賴注射!或者,你可以手動的傳遞物件,這不是太理想,或者,你可以整合Dagger 2(比Dagger1好多了因為都是在編譯階段)。我儘量避免手動完成:你頂層架構類中(主要是在檢視層中)有大量的方法,這個類建立和傳遞物件,這些物件只是在你低層次部分的架構中(**哎**)。想象一個Fragment, 建立了一個APIService,把它傳遞到所有的層級中!太糟糕了!
結論
當你遷移你程式碼到Rxjava2,確保你以自己的想要的方式使用Streams和Observers。
這是一個很好的總結: 怎麼用MVVM構建你的應用和以高效的方式處理Views的生命週期