原文: 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。我们也将讲到,利用网络请求响应到视图层的生命周期,怎么提高你应用的性能。
应用结构
如果我们快速了解下不同层...
在这篇文章中,我将大量论述一个小项目,你将看见一切是怎么实现的
manuelvicnt/RxJava2-MVVM-Android-Structure
在上个用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();
}
}
这样,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();
}
如果我们想把response返回到RequestManager层,但是因为这是个网络请求,而且我们知道我们将收到一个对象,我们可以使用Maybe(有可能,body是空的,所以当null对象时,我们使用Maybe来避免异常)
记住,用singleElement()操作子,而不是singleElement()操作子。如果你使用第二个,你获取不到什么,它将抛一个异常,因为它一直会尝试获取第一个元素,即使没有第一个元素。
public Maybe 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 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
private Flowable getAccount() {
return accountAPIService.getAccount(createAccountRequest());
}
private Flowable getGames() {
return gamesAPIService.getGames(createGamesRequest());
}
连接不同的网络请求
我们看到怎么每个网络请求返回不同类型的Stream。让我们看看我们怎么连接它们。计划是,Registration请求,Login请求,然后是UserData。进行多合一。
UserData返回Flowable。然而,Login请求返回Maybe。我们必须匹配它们。
#AuthenticationRequestManager.java
private MaybeSource
如果响应是成功的,Login请求将获取UserData。我们预计getUserDataRequestMethod返回Maybe,我们可以用flatMap()操作子连接它们。
#AuthenticationRequestManager.java
public MaybeSource
现在如果我们想调用Registration,然后是Login请求,我们仅仅在Completable完成后调动Login请求。我们用andThen()操作子来完成
#AuthenticationRequestManager.java
public MaybeSource
private MaybeSource
如果我们看看文档,我们能发现,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 extends DisposableMaybeObserver {
@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
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
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
如果你看看这个工程,我用一个客户端来模拟网络请求,添加延时,所以当应用回到后台时我们能测试它。
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的生命周期