内容简介:原文:作者:Hannes Dorfmann译者:
《使用MVI打造响应式APP》翻译系列
- [译]使用MVI打造响应式APP(一):Model到底是什么
- [译]使用MVI打造响应式APP[二]:View层和Intent层
- [译]使用MVI打造响应式APP[三]:状态合并
- [译]使用MVI打造响应式APP[四]:IndependentUIComponents
- [译]使用MVI打造响应式APP[五]:DebuggingWithEase
- [译]使用MVI打造响应式APP[六]:RestoringState
- [译]使用MVI打造响应式APP[七]:Timing,SingleLiveEventProblem
- [译]使用MVI打造响应式APP[八]:Navigation
《使用MVI打造响应式APP》实战系列
------------------------------正文分割线-------------------------------
原文: REACTIVE APPS WITH MODEL-VIEW-INTENT - PART2 - VIEW AND INTENT
作者:Hannes Dorfmann
译者: 却把清梅嗅
在 上文 中,我们探讨了对 Model
的定义、与 状态 的关系以及如何在通过良好地定义 Model
来解决一些 Android
开发中常见的问题。本文将通过 Model-View-Intent
,即 MVI
模式,继续我们的 响应式App 构建之旅。
如果您尚未阅读上一小节,则应在继续阅读本文之前阅读该部分。总结一下:以“传统的” MVP
为例,请避免写出这样的代码:
class PersonsPresenter extends Presenter<PersonsView> { public void load(){ getView().showLoading(true); // 展示一个 ProgressBar backend.loadPersons(new Callback(){ public void onSuccess(List<Person> persons){ getView().showPersons(persons); // 展示用户列表 } public void onError(Throwable error){ getView().showError(error); // 展示错误信息 } }); } } 复制代码
我们应该创建能够反映 状态 的 Model
,像这样:
class PersonsModel { // 在真实的项目中,需要定义为私有的 // 并且我们需要通过getter和setter来访问它们 final boolean loading; final List<Person> persons; final Throwable error; public(boolean loading, List<Person> persons, Throwable error){ this.loading = loading; this.persons = persons; this.error = error; } } 复制代码
因此, Presenter
层也应该像这样进行定义:
class PersonsPresenter extends Presenter<PersonsView> { public void load(){ getView().render( new PersonsModel(true, null, null) ); // 展示一个 ProgressBar backend.loadPersons(new Callback(){ public void onSuccess(List<Person> persons){ getView().render( new PersonsModel(false, persons, null) ); // 展示用户列表 } public void onError(Throwable error){ getView().render( new PersonsModel(false, null, error) ); // 展示错误信息 } }); } } 复制代码
现在,仅需简单调用 View
层的 render(personsModel)
方法, Model
就会被成功的渲染在屏幕上。在第一小节中我们同样探讨了 单项数据流 的重要性,同时您的业务逻辑应该驱动该 Model
。在正式将所有内容环环相扣连接之前,我们先简单了解一下 MVI
的核心思想。
Model-View-Intent (MVI)
该模式最初被andrestaltz 在他写的 JavaScript
框架cycle.js 中所提出; 从理论(还有数学)上讲,我们这样对 Model-View-Intent
的定义进行描述:
1.intent()
此函数接受来自用户的输入(即UI事件,比如点击事件)并将其转换为可传递给 Model()
函数的参数,该参数可能是一个简单的 String
对 Model
进行赋值,也可能像是 Object
这样复杂的数据结构。 intent
作为意图,标志着 我们试图对 Model
进行改变 。
2.model()
model()
函数将 intent()
函数的输出作为输入来操作 Model
,其函数输出是一个新的 Model
(状态发生了改变)。
不要对已存在的 Model
对象进行修改,我们需要的是不可变 !对此,在上文中我们已经展示了一个计数器的具体案例,再次重申,不要修改已存在的 Model
!
根据 intent
所描述的变化,我们创建一个新的 Model
,请注意, Model()
函数是唯一允许对 Model
进行创建的途径。然后这个新的 Model
作为该函数的输出——基本上 model()
函数调用我们 App
的业务逻辑(可以是交互、用例、 Repository
......您在 App
中使用的任何模式/术语)并作为结果提供新的 Model
对象。
3.view()
该方法获取 model()
函数返回的 Model
,并将其作为 view()
函数的输入,这之后通过某种方式将 Model
展示出来, view()
和 view.render(model)
大体上是一致的。
4.本质
但是我们希望构建的是 响应式的App ,不是吗?那么 MVI
是如何响应式的呢?响应式实际上意味着什么?
这意味着 App
的 UI反映了状态的变更 。
因为 Model
反映了状态,因此,本质上我们希望 业务逻辑能够对输入的事件(即 intents
)进行响应,并创建对应的 Model
作为输出,这之后再通过调用 View
层的 render(model)
方法,对UI进行渲染 。
5.通过RxJava串联
我们希望我们的数据流的单向性,因此 RxJava
闪亮登场。我们的App必须通过 RxJava
保持 数据的单向性 和 响应式 来构建吗?或者必须用 MVI
模式才能构建吗?当然不,我们也可以写 命令式 和 程序性 的代码。但是, 基于事件编程 的 RxJava
实在太优秀了,既然 UI
是基于事件的,因此使用 RxJava
也是非常有意义的。
本文我们将会构建一个简单的虚拟在线商店 App
,其UI界面中展示的商品数据,都来源于我们向后台进行的网络请求。
我们可以精确的搜索特定的商品,并将其添加到我们的购物车中,最终 App
的效果如下所示:
这个项目的源码你可以在 Github 上找到,我们从实现一个简单的搜索界面开始做起:
首先,就像上文我们描述的那样,我们定义一个 Model
用于描述 View
层是如何被展示的—— 这个系列中,我们将用带有 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<Product> result; public SearchResult(String searchQueryText, List<Product> result) { this.searchQueryText = searchQueryText; this.result = result; } public String getSearchQueryText() { return searchQueryText; } public List<Product> 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
类型的对象,它可能是 SearchViewState.Error
或者其它的一个实例。这只是我个人的偏好,们也可以通过不同的方式定义,例如:
class SearchViewState { Throwable error; // 非空则意味着,出现了一个错误 boolean loading; // 值为true意味着正在加载中 List<Product> result; // 非空意味着商品列表的结果 boolean SearchNotStartedYet; // true意味着还未开始搜索 } 复制代码
再次重申,如何定义 Model
纯属个人喜好,如果你用 Kotlin
作为编程语言,那么 sealed classes
是一个不错的选择。
将目光聚集回到业务代码,让我们通过 SearchInteractor
去执行搜索的功能,其输出就是我们之前说过的 SearchViewState
对象:
public class SearchInteractor { final SearchEngine searchEngine; // 执行网络请求 public Observable<SearchViewState> search(String searchString) { // 如果是空的字符串,不进行搜索 if (searchString.isEmpty()) { return Observable.just(new SearchViewState.SearchNotStartedYet()); } // 搜索商品列表 // 返回 Observable<List<Product>> return searchEngine.searchFor(searchString) .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()
的方法签名:我们将 String
类型的 searchString
作为 输入 的参数,以及 Observable<SearchViewState>
类型的 输出 ,这意味着我们期望随着时间的推移,可以在可观察的流上会有任意多个 SearchViewState
的实例被发射。
在我们正式开始查询搜索之前(即 SearchEngine
执行网络请求),我们通过 startWith()
操作符发射一个 SearchViewState.Loading
,这将会使得 View
在执行搜索时展示 ProgressBar
。
onErrorReturn()
会捕获在执行搜索时抛出的所有异常,并且发射出一个 SearchViewState.Error
——在订阅这个 Observable
时,我们为什么不去使用 onError()
回调呢?
这是一个对 RxJava
认知的普遍误解,实际上, onError()
的回调意味着 整个可观察的流进入了不可恢复的状态 ,因此可观察的流结束了,而在我们的案例中,类似“没有网络连接”的 error
并非不可恢复的 error
:这只是我们的 Model
所代表的另外一个状态。
此外,我们还有另外一个可以转换到的状态,即一旦网络连接可用,我们可以通过 SearchViewState.Loading
跳转到的 加载状态 。
因此,我们建立了一个可观察的流,这是一个每当状态发生了改变,从业务逻辑层就会发射一个发生了改变的 Model
到 View
层的流。
我们不想在网络连接错误时终止这个可观察的流,因此,在 error
发生时,类似这种可以被处理为 状态 的 error
(而不是终止流的那种致命的错误),可以反应为 Model
,被可观察的流发射。
通常,在 MVI
中, Model
的 Observable
永远不会被终止(即永远不会执行 onComplete()
或者 onError()
回调)。
总结一下, SearchInteractor
(即业务逻辑)提供了一个可观察的流 Observable<SearchViewState>
,每当状态发生了变化,就会发射一个新的 SearchViewState
。
6.View层的职责
接下来我们来讨论一下 View
应该是什么样的, View
层的职责是什么?显然 View
层应该对 Model
进行展示,我们已经认可 View
层应该有类似 render(model)
这样的函数。此外, View
应该提供一个给其他层响应用户输入的方法,在 MVI
中这个方法被称为 intents
。
在这个案例中,我们只有一个 intent
:用户可以在输入框中输入一个用于检索商品的字符串进行搜索。 MVP
中的好习惯是为 View
层定义一个接口,所以在 MVI
中我们也可以这样做。
public interface SearchView { // 搜索的intent Observable<String> searchIntent(); // 对View层进行渲染 void render(SearchViewState viewState); } 复制代码
我们的案例中 View
层只提供了一个 intent
,但通常 View
拥有更多的 intent
;在 第一小节 中我们讨论了为什么一个单独的 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<String> searchIntent() { return RxSearchView.queryTextChanges(searchView) // 感谢 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<Product> 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
的 RxBinding ,这是一个对 Android UI
组件提供了 RxJava
响应式支持的库。
RxSearchView.queryText()
创建了一个 Observable<String>
,每当用户在 EditText
上输入了一些文字,它就会发射一个对应的字符串;我们通过 filter()
去保证只有当用户输入的字符数达到三个以上时才进行搜索;同时,我们不希望每当用户输入一个字符,就去请求网络,而是当用户输入结束后再去请求网络( debounce()
操作符会停留500毫秒以决定用户是否输入完成)。
现在我们知道了屏幕中的 searchIntent()
方法就是 输入 ,而 render()
方法则是 输出 。我们如何从 输入 获得 输出 呢,如下所示:
7.连接View和Intent
剩下的问题就是:我们如何将 View
的 intent
和业务逻辑进行连接呢?如果你认真观看了上面的流程图,你应该注意到了中间的 flatMap()
操作符,这暗示了我们还有一个尚未谈及的组件: Presenter
; Presenter
负责连接这些点,就和我们在 MVP
中使用的方式一样。
public class SearchPresenter extends MviBasePresenter<SearchView, SearchViewState> { private final SearchInteractor searchInteractor; @Override protected void bindIntents() { Observable<SearchViewState> search = intent(SearchView::searchIntent) // 上文中我们谈到了flatMap,但在这里switchMap更为适用 .switchMap(searchInteractor::search) .observeOn(AndroidSchedulers.mainThread()); subscribeViewState(search, SearchView::render); } } 复制代码
什么是 MviBasePresenter
, intent()
和 subscribeViewState()
又是什么?这个类是我写的 Mosby 库的一部分(3.0版本后, Mosby
已经支持了 MVI
)。本文并非为了讲述 Mosby
,但我向简单介绍一下 MviBasePresenter
是如何的便利——这其中没有什么黑魔法,虽然确实看起来像是那样。
让我们从生命周期开始: MviBasePresenter
并未持有任何生命周期,它暴露出一个 bindIntent()
方法以供 View
层和业务逻辑进行绑定。通常,你通过 flatMap()
、 switchMap()
或者 concatMap()
操作符将 intent
“转移”到业务逻辑中,这个方法仅仅在 View
层第一次被附加到 Presenter
中时调用,而当 View
再次被附加在 Presenter
中时(比如,屏幕方向发生了改变),将不再被调用。
这听起来有些怪,也许有人会说:
MviBasePresenter
在屏幕方向发生了改变后依然能够存活?如果是这样, Mosby
如何保证 Observable
的流不会发生内存的泄漏?
这就是 intent()
和 subscribeViewState()
的作用所在了, intent()
在内部创建一个 PublishSubject
,就像是业务逻辑的“网关”一样;实际上, PublishSubject
订阅了 View
层传过来的 intent
的 Observable
,调用 intent(o1)
实际返回了一个订阅了 o1
的 PublishSubject
。
屏幕发生旋转时, Mosby
将 View
从 Presenter
中分离,但是,内部的 PublishSubject
只是暂时和 View
解除了订阅;而当 View
重新附着在 Presenter
上时, PublishSubject
将会对 View
层的 intent
进行重新订阅。
subscribeViewState()
方法做的是同样的事情,只不过将顺序调换了过来( Presenter
向 View
层的通信)。它在内部创建一个 BehaviorSubject
作为从业务逻辑到View层的“网关”。
由于它是一个 BehaviorSubject
,因此,即使此时 Presenter
没有持有 View
,我们依然可以从业务逻辑中接收到 Model
的更新(比如 View
并未处于栈顶); BehaviorSubjects
始终持有它最后的值,并在 View
重新依附后将其重新发射。
规则很简单:使用 intent()
来“包装” View
层的所有 intent
,使用 subscribeViewState()
替代 Observable.subscribe()
.
8.UnbindIntents
与 bindIntent()
相对应的是 unbindIntents()
,该方法只会执行一次,即 View
被永久销毁时才会被调用。举个例子,将一个 Fragment
放在栈中,直到 Activity
被销毁之前,该 View
一直不会被销毁。
由于 intent()
和 subscribeViewState()
已经对订阅进行了管理,因此您只需要实现 unbindIntents()
。
9.其它生命周期的事件
那么其它生命周期的事件,比如 onPause()
和 onResume()
又该如何处理?我依然认为 Presenter
不需要生命周期的事件,然而,如果你坚持认为你需要将这些生命周期的事件视为另一种形式的 intent
,您的 View
可以提供一个 pauseIntent()
,它是由 android
生命周期触发,而又不是按钮点击事件这样的由用户交互触发的 intent
——但两者都是有效的意图。
结语
第二小节中,我们探讨了 Model-View-Intent
的基础,并通过 MVI
浅尝辄止实现了一个简单的页面。也许这个例子太简单了,所以你尚未感受到 MVI
模式的优点:代表 状态 的 Model
和与传统 MVP
或者 MVVM
相比的 单项数据流 。
MVP
和 MVVM
并没有什么问题,我也并非是在说 MVI
比其它架构模式更优秀,但是,我认为 MVI
可以帮助我们 为复杂的问题编写优雅的代码 ,这也正如我们将在本系列博客的 下一小节 (第3小节)中探讨的那样——届时我们将针对 状态合并 (state reducers)的问题进行探讨,欢迎关注。
--------------------------广告分割线------------------------------
关于我
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 :heart:,也欢迎关注我的博客或者 Github 。
如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢?
以上所述就是小编给大家介绍的《[译]使用MVI打造响应式APP(二):View层和Intent层》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 使用 Reactor 响应式编程
- 使用Reactor响应式编程
- 使用 RxSwift 进行响应式编程
- qt – 如何使用QListWidget响应内部拖放操作?
- android – 使用Retrofit&Gson解析JSON数组响应
- 使用Elasticsearch与TheHive构建开源安全应急响应平台
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人件(原书第3版)
[美] Tom DeMarco、[美] Timothy Lister / 肖然、张逸、滕云 / 机械工业出版社 / 2014-8 / 69.00元
在软件管理领域,很少有著作能够与本书媲美。作为经久不衰的畅销书,本书深刻地洞察到软件开发的最大问题不在于技术,而在于人。人的因素并不容易解决,一旦解决了,你将更有可能获得成功。 本书是软件管理领域的传奇经典,被誉为“对美国软件业影响最大的一本书”。全书从管理人力资源、创建健康的办公环境、雇用并留用正确的人、高效团队形成、改造企业文化和快乐工作等多个角度阐释了如何思考和管理软件开发的最大问题—......一起来看看 《人件(原书第3版)》 这本书的介绍吧!