1. 程式人生 > >RxJava 2: 用Retrofit2架構Android MVVM 生命週期

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

。當持有資訊時,Subject相當有幫助,直到View顯示來獲取。

如果你看一下新的程式碼倉庫,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,等等。

記住,CompletableSingleMaybe不處理背壓。

為了學習的目的,我們將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的生命週期