在第一部分我们讨论了关于什么才是真正的Model,Model和状态的关系,并且讨论了什么样的Model才能避免安卓开发过程中的共性问题。在这篇我们通过讲Model-View-Intent模式去构建响应式安卓程序,继续我们的“响应式APP开发”探索之旅。
如果你没有阅读第一部分,你应该先读那篇然后再读这篇。我在这里先简单的回顾一下上一部分的主要内容:我们不要写类似于下面的代码(传统的MVP的例子)
class PersonsPresenter extends Presenter {
public void load(){
getView().showLoading(true); // Displays a ProgressBar on the screen
backend.loadPersons(new Callback(){
public void onSuccess(List persons){
getView().showPersons(persons); // Displays a list of Persons on the screen
}
public void onError(Throwable error){
getView().showError(error); // Displays a error message on the screen
}
});
}
}
我们应该创建一个反应"状态(State)"的"Model":
class PersonsModel {
// 在正式的项目里应当为私有
// 我们需要用get方法来获取它们的值
final boolean loading;
final List persons;
final Throwable error;
public(boolean loading, List persons, Throwable error){
this.loading = loading;
this.persons = persons;
this.error = error;
}
}
然后Presenter的实现类似于下面这样:
class PersonsPresenter extends Presenter {
public void load(){
getView().render( new PersonsModel(true, null, null) ); //显示加载进度条
backend.loadPersons(new Callback(){
public void onSuccess(List persons){
getView().render( new PersonsModel(false, persons, null) ); // 显示人列表
}
public void onError(Throwable error){
getView().render( new PersonsModel(false, null, error) ); // 显示错误信息
}
});
}
}
现在View有一个Model,通过调用render(personsModel) 方法,将数据渲染到UI上。在上一篇文章里我们也讨论了单向数据流的重要性,并且你的业务逻辑应当驱动你的Model。在我们把所有的内容连起来之前,我们先快速的了解一下MVI的大意。
Model-View-Intent(MVI)
这个模式被 André Medeiros (Staltz) 为了他写的一个JavaScript的框架而提出的,这个框架的名字叫做 cycle.js 。从理论上(数学上)来看,我们可以用下面的表达式来描述Model-View-Intent:
- intent() :这个函数接受用户的输入(例如,UI事件,像点击事件之类的)并把它转化成model函数的可接收的参数。这个参数可能是一个简单的String,也可能是其他复杂的结构的数据,像Object。我们可以说我们通过intent()的意图去改变Model。
- model() :model()函数接收intent()函数的输出作为输入,去操作Model。它的输出是一个新的Model(因为状态改变)。因此我们不应该去更新已经存在的Model。因为我们需要Model具有不变性! 在第一部分,我具体用”计数APP“作为简单的例子讲了数据不变性的重要性。再次强调,我们不要去修改已经存在的Model实例。我们在model()方法里创建新的,根据intent的输出变化以后的Model。请注意,model()方法是你唯一能够创建新的Model对象的地方。基本上,我们称model()方法为我们App的业务逻辑(可以是Interactor,Usecase,Repository ...您在应用中使用的任何模式/术语)并且传递新的Model对象作为结果。
- view() :这个方法接收model()方法的输出值。然后根据model()的输出值来渲染到UI上。view()方法大致上类似于view.render(model) 。
但是,我们不是去构建一个”响应式的APP“,不是么?所以,MVI是如何做到"响应式"的?"响应式"到底意味着什么?先回答最后一个问题,”响应式“就是我们的app根据状态不同而去改变UI。在MVI中,”状态“被"Model"所代表,实质上我们期望,我们的业务逻辑根据用户的输入事件(intent)产生新的"Model",然后再将新的"Model"通过调用view的render(Model)方法改变在UI。这就是MVI实现响应式的基本思路。
使用RxJava来连接不同的点(这里的点是指☞Model,View,Intent原本是相互独立的点)
我们想要让我们的数据流是单向的。RxJava在这里起到了作用。我们必须使用RxJava构建单向数据流的响应式App或MVI模式的App么?不是的,我们可以用其他的代码实现。然而,RxJava对于事件基础的编程是很好用的。既然用户界面是基于事件的,使用RxJava也就很有意义的。
在这个系列博客,我们将要开发一个简单的电商应用。我们在后台进行http请求,去加载我们需要显示商品。我们可以搜索商品和添加商品到购物车。综上所述整个App看起来想下面这个动图:
这个项目的源代码你可以在 github 上找到。我们先去实现一个简单的页面:实现搜索页面。首先,我们先定义一个最终将被View显示的Model。在这个系列博客我们采用"ViewState"标示来标示Model ,例如:我们的搜索页面的Model类叫做SearchViewState ,因为Model代表状态(State)。至于为什么不使用SearchModel这样的名字,是因为怕与MVVM的类似于SearchViewModel的命名混淆。命名真的很难。
public interface SearchViewState {
/**
*搜索还没有开始
*/
final class SearchNotStartedYet implements SearchViewState {
}
/**
* 加载: 等待加载
*/
final class Loading implements SearchViewState {
}
/**
*标识返回一个空结果
*/
final class EmptyResult implements SearchViewState {
private final String searchQueryText;
public EmptyResult(String searchQueryText) {
this.searchQueryText = searchQueryText;
}
public String getSearchQueryText() {
return searchQueryText;
}
}
/**
* 验证搜索结果. 包含符合搜索条件的项目列表。
*/
final class SearchResult implements SearchViewState {
private final String searchQueryText;
private final List result;
public SearchResult(String searchQueryText, List result) {
this.searchQueryText = searchQueryText;
this.result = result;
}
public String getSearchQueryText() {
return searchQueryText;
}
public List getResult() {
return result;
}
}
/**
*标识搜索出现的错误状态
*/
final class Error implements SearchViewState {
private final String searchQueryText;
private final Throwable error;
public Error(String searchQueryText, Throwable error) {
this.searchQueryText = searchQueryText;
this.error = error;
}
public String getSearchQueryText() {
return searchQueryText;
}
public Throwable getError() {
return error;
}
}
}
Java是个强类型的语言,我们需要为我们的Model选择一个安全的类型。我们的业务逻辑返回的是 SearchViewState 类型的。当然这种定义方法是我个人的偏好。我们也可以通过不同的方式定义,例如:
class SearchViewState {
Throwable error; // if not null, an error has occurred
boolean loading; // if true loading data is in progress
List result; // if not null this is the result of the search
boolean SearchNotStartedYet; // if true, we have the search not started yet
}
再次强调,你可以按照你的方式来定义你的Model。如果,你会使用kotlin语言的话,那么sealed classes是一个很好的选择。
下一步,让我将聚焦点重新回到业务逻辑。让我们看一下负责执行搜索的 SearchInteractor 如何去实现。先前已经说过了它的"输出"应该是一个 SearchViewState 对象。
public class SearchInteractor {
final SearchEngine searchEngine; // 进行http请求
public Observable search(String searchString) {
// 空的字符串,所以没搜索
if (searchString.isEmpty()) {
return Observable.just(new SearchViewState.SearchNotStartedYet());
}
// 搜索商品
return searchEngine.searchFor(searchString) // Observable>
.map(products -> {
if (products.isEmpty()) {
return new SearchViewState.EmptyResult(searchString);
} else {
return new SearchViewState.SearchResult(searchString, products);
}
})
.startWith(new SearchViewState.Loading())
.onErrorReturn(error -> new SearchViewState.Error(searchString, error));
}
}
让我们看一下SearchInteractor.search()的方法签名:我们有一个字符串类型的searchString作为输入参数,和Observable
onErrorReturn() 捕获所有的在执行搜索的时候出现的异常,并且,发射一个SearchViewState.Error 。当我们订阅这个Observable的时候,我们为什么不只用onError的回调?这是对RxJava一个共性的误解:onError回调意味着我们整个观察流进入了一个不可恢复的状态,也就是整个观察流已经被终止了。但是,在我们这里的错误,像无网络之类的,不是不可恢复的错误。这仅仅是另一种状态(被Model代表)。此外,之后,我们可以移动到其他状态。例如,一旦我们的网络重新连接起来,那么我们可以移动到被SearchViewState.Loading 代表的“加载状态”。因此,我们建立了一个从我们的业务逻辑到View的观察流,每次发射一个改变后的Model,我们的"状态"也会随着改变。我们肯定不希望我们的观察流因为网络错误而终止。因此,这类错误被处理为一种被Model代表的状态(除去那些致命错误)。通常情况下,在MVI中可观察对象Model不会被终止(永远不会执行onComplete()或onError())。
对上面部分做个总结:SearchInteractor(业务逻辑)提供了一个观察流Observable
下一步,让我讨论View层长什么样子的。View层应该做什么?显然的,view应该去显示Model。我们已经同意,View应当有一个像render(model) 这样的方法。另外,View需要提供一个方法给其他层用来接收用户输入的事件。这些事件在MVI中被称作 intents 。在这个例子中,我们仅仅只有一个intent:用户可以通过在输入区输入字符串来搜索。在MVP中一个好的做法是我们可以为View定义接口,所以,在MVI中,我们也可以这样做。
public interface SearchView {
/**
* The search intent
*
* @return An observable emitting the search query text
*/
Observable searchIntent();
/**
* Renders the View
*
* @param viewState The current viewState state that should be displayed
*/
void render(SearchViewState viewState);
}
在这种情况下,我们的View仅仅提供一个intent,但是,在其他业务情况下,可能需要多个intent。在第一部分我们讨论了为什么单个render()方法(译者:渲染方法)是一个好的方式,如果,你不清楚为什么我们需要单个render(),你可以先去阅读第一部分。在我们具体实现View层之前,我们先看一下最后搜索页面是什么样的
public class SearchFragment extends Fragment implements SearchView {
@BindView(R.id.searchView) android.widget.SearchView searchView;
@BindView(R.id.container) ViewGroup container;
@BindView(R.id.loadingView) View loadingView;
@BindView(R.id.errorView) TextView errorView;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.emptyView) View emptyView;
private SearchAdapter adapter;
@Override public Observable searchIntent() {
return RxSearchView.queryTextChanges(searchView) // Thanks Jake Wharton :)
.filter(queryString -> queryString.length() > 3 || queryString.length() == 0)
.debounce(500, TimeUnit.MILLISECONDS);
}
@Override public void render(SearchViewState viewState) {
if (viewState instanceof SearchViewState.SearchNotStartedYet) {
renderSearchNotStarted();
} else if (viewState instanceof SearchViewState.Loading) {
renderLoading();
} else if (viewState instanceof SearchViewState.SearchResult) {
renderResult(((SearchViewState.SearchResult) viewState).getResult());
} else if (viewState instanceof SearchViewState.EmptyResult) {
renderEmptyResult();
} else if (viewState instanceof SearchViewState.Error) {
renderError();
} else {
throw new IllegalArgumentException("Don't know how to render viewState " + viewState);
}
}
private void renderResult(List result) {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.VISIBLE);
loadingView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
adapter.setProducts(result);
adapter.notifyDataSetChanged();
}
private void renderSearchNotStarted() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
private void renderLoading() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.VISIBLE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
}
private void renderError() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
private void renderEmptyResult() {
TransitionManager.beginDelayedTransition(container);
recyclerView.setVisibility(View.GONE);
loadingView.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
render(SearchViewState) 这个方法,我们通过看,就知道它是干什么的。在 searchIntent() 方法中我们用到了Jake Wharton’s的RxBindings 库,它使RxJava像绑定可观察对象一样绑定安卓UI控件。 RxSearchView.queryText()创建一个 Observable
因此,我们知道对于这个页面而言,输入是searchIntent(),输出是render()。我们如何从“输入”到“输出”?下面的视频将这个过程可视化了:
其余的问题是谁或如何把我们的View的意图(intent)和业务逻辑联系起来?如果你已经看过了上面的视频,可以看到在中间有一个RxJava的操作符 flatMap() 。这暗示了我们需要调用额外的组件,但是,我们至今为止还没有讨论,它就是 Presenter 。Presenter将所有分离的不同点(译者:这里指Model,View,Intent这三个点)联系起来。它与MVP中的Presenter类似。
public class SearchPresenter extends MviBasePresenter {
private final SearchInteractor searchInteractor;
@Override protected void bindIntents() {
Observable search =
intent(SearchView::searchIntent)
.switchMap(searchInteractor::search) // 我在上面视频中用flatMap()但是 switchMap() 在这里更加适用
.observeOn(AndroidSchedulers.mainThread());
subscribeViewState(search, SearchView::render);
}
}
MviBasePresenter 是什么?这个是我写的一个库叫 Mosby (Mosby3.0已经添加了MVI组件)。这篇博客不是为介绍Mosby而写的,但是,我想对MviBasePresenter做个简短的介绍。介绍一下MviBasePresenter如何让你方便使用的。这个库里面没有什么黑魔法。让我们从lifecycle(生命周期)开始说:MviBasePresenter事实上没有lifecyle(生命周期)。有一个 bindIntent() 方法将视图的意图(intent)与业务逻辑绑定。通常,你用flatMap()或switchMap 亦或concatMap(),将意图(intent)传递给业务逻辑。这个方法的调用仅仅在View第一次被附加到Presenter。当View重新附加到Presenter时,将不会被调用(例如,当屏幕方向改变)。
这听起来很奇怪,也许有人会说:“MviBasePresenter在屏幕方向变化的时候都能保持?如果是的话,Mosby是如何确保可观察流的数据在内存中,而不被丢失?”,这是intent()和 subscribeViewState() 的就是用来回答这个问题的。intent() 在内部创建一个PublishSubject ,并将其用作你的业务逻辑的“门户”。所以实际上这个PublishSubject订阅了View的意图(intent)可观察对象( Observable)。调用intent(o1)实际上返回一个订阅了o1的PublishSubject。
当方向改变的时候,Mosby从Presenter分离View,但是,仅仅只是暂时的取消订阅内部的PublishSubject。并且,当View重新连接到Presenter的时候,将PublishSubject重新订阅View的意图(intent)。
subscribeViewState() 用不同的方式做的是同样的事情(Presenter到View的通信)。它在内部创建一个BehaviorSubject 作为业务逻辑到View的“门户”。既然是BahaviorSubject,我们可以从业务逻辑收到“模型更新”的信息,即使是目前没有view附加(例如,View正处于返回栈)。BehaviorSubjects总是保留最后时刻的值,每当有View附加到上面的时候,它就开始重新接收,或者将它保留的值传递给View。
规则很简单:用intent()去“包装”所有View的意图(点击事件等)。用subscribeViewState()而不是Observable.subscribe(...)。
和bindIntent()对应的是unbindIntents() ,这两个方法仅仅会被调用一次,当unbindIntents()调用的时候,那么View就会被永久销毁。举个例子,将fragment处于返回栈,不去永久销毁view,但是如果一个Activity结束了它的生命周期,就会永久销毁view。由于intent()和subscribeViewState()已经负责订阅管理,所以你几乎不需要实现unbindIntents()。
那么关于我们生命周期中的onPause() 和 onResume() 是如何处理的?我认为Presenters是不需要关注生命周期 。如果,你非要在Presenter中处理生命周期,比如你将onPause()作为intent。你的View需要提供一个pauseIntent() 方法,这个方法是由生命周期触发的,而不是用户交互触发的,但两者都是有效的意图。
总结
在第二部分,我们讨论了关于Model-View-Intent的基础,并且用MVI实现了一个简单的搜索页面。让我们入门。也许这个例子太简单了。你无法看出MVI的优势,Model代表状态和单向数据流同样适用于传统的MVP或MVVM。MVP和MVVM都很优秀。MVI也许并没有它们优秀。即使如此,我认为MVI帮助我们面对复杂问题的时候写优雅的代码。我们将在这个系列博客第三部分,讨论状态减少。