什么是狀態(tài)?界面上展示給用戶的都是一種狀態(tài),如loading顯示葵姥,error信息顯示,列表展示等句携。這是日常開(kāi)發(fā)中必然會(huì)遇到的榔幸,本文將講解如何用更有效的方式來(lái)進(jìn)行狀態(tài)管理,提高代碼的可讀性矮嫉,可維護(hù)性削咆,健壯性。蠢笋。拨齐。。昨寞。瞻惋。文章中代碼示例比較多,但是別慌援岩,邏輯都比較簡(jiǎn)單歼狼,穩(wěn)住就行。文章代碼使用kotlin實(shí)現(xiàn)享怀,關(guān)于狀態(tài)管理部分示例代碼使用MVP
+ RxJava
模式來(lái)編寫(xiě)羽峰。
關(guān)于狀態(tài)管理
假設(shè)我們有這樣一個(gè)需求:在輸入框輸入用戶名,點(diǎn)擊保存按鈕把用戶保存到數(shù)據(jù)庫(kù)添瓷。在保存數(shù)據(jù)庫(kù)之前梅屉,顯示loading狀態(tài),然后把保存按鈕設(shè)置為不可點(diǎn)擊仰坦,保存數(shù)據(jù)庫(kù)需要異步操作履植,最后在成功的時(shí)候隱藏loading狀態(tài)并且把保存按鈕設(shè)置為可點(diǎn)擊,若發(fā)生錯(cuò)誤悄晃,需要隱藏loading狀態(tài)玫霎,把保存按鈕設(shè)置為可點(diǎn)擊狀態(tài)凿滤,然后顯示錯(cuò)誤信息。Show you the code:
class MainPresenter constructor(
private val service: UserService,
private val view: MainView,
private val disposables: CompositeDisposable
) : Presenter {
val setUserSubject = PublishSubject.create<String>()
init {
disposables.add(
setUserSubject
.doOnNext {
view.showLoading()
view.setButtonUnable()
}
.flatMap { service.setUser(it) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
view.hideLoading()
view.setButtonEnable()
},
{
view.hideLoading()
view.setButtonEnable()
view.showError(it.message.toString())
}
))
}
override fun setUser(userName: String) {
setUserSubject.onNext(userName)
}
}
這段代碼看上去不怎么優(yōu)雅庶近,但已經(jīng)實(shí)現(xiàn)了我們的需求了翁脆。簡(jiǎn)單畫(huà)下流程圖:
可以看到當(dāng)保存數(shù)據(jù)庫(kù)操作前調(diào)用view.showLoading()
和view.setButtonUnable()
,當(dāng)操作成功或者錯(cuò)誤的時(shí)候調(diào)用view.hideLoading()
和view.setButtonEnable()
鼻种,像這種“配套”方法越來(lái)越多的時(shí)候就很容易會(huì)疏忽反番,出現(xiàn)忘記隱藏loading狀態(tài),忘記把按鈕設(shè)置為可點(diǎn)擊等問(wèn)題叉钥。在這簡(jiǎn)單例子你可能會(huì)覺(jué)得沒(méi)什么罢缸,實(shí)際開(kāi)發(fā)的時(shí)候一定會(huì)記得調(diào)用相應(yīng)的方法,這不同于注冊(cè)接口監(jiān)聽(tīng)投队,一般我們會(huì)在Activity#onCreate()
的時(shí)候注冊(cè)監(jiān)聽(tīng)枫疆,在Activity#onDestroy()
取消監(jiān)聽(tīng),但我們?cè)?strong>View里可以有很多地方調(diào)用Presenter的方法敷鸦,如setUser()
息楔,我們認(rèn)為調(diào)用Presenter方法是一種輸入,同時(shí)Presenter也有很多地方輸出狀態(tài)給View扒披,如view.showLoading()
值依,view.showError()
等。我們不能確定setUser()
方法在哪里被調(diào)用碟案,view.showLoading()
方法在哪里被調(diào)用愿险,假設(shè)我們還有其他方法在同時(shí)執(zhí)行:
這很容易會(huì)造成狀態(tài)混亂,例如loading狀態(tài)和錯(cuò)誤信息同時(shí)出現(xiàn)价说,當(dāng)錯(cuò)誤信息顯示的時(shí)候保存按鈕沒(méi)有恢復(fù)可點(diǎn)擊狀態(tài)等拯啦,在實(shí)際的業(yè)務(wù)中,這種問(wèn)題尤其明顯熔任。
響應(yīng)式狀態(tài)(Reative State)
我們能不能限制Presenter只有一個(gè)輸入,狀態(tài)只從一個(gè)地方輸出呢唁情?我們借助PublishSubject
作為橋接(如上面代碼片段setUserSubject
)疑苔,然后通過(guò)Observable.merge()
把它們合并成一個(gè)流,來(lái)實(shí)現(xiàn)只有一個(gè)地方輸入甸鸟。下面我們主要看看我們?nèi)绾螌?shí)現(xiàn)狀態(tài)只從一個(gè)地方輸出惦费。
引用面向?qū)ο缶幊桃痪浣?jīng)典的話:萬(wàn)物皆對(duì)象。用戶輸入用戶名抢韭,點(diǎn)擊保存按鈕薪贫,這是一個(gè)事件,我們把它看成一個(gè)事件對(duì)象SetUserEvent
刻恭,把UI狀態(tài)作為一個(gè)狀態(tài)對(duì)象(SetUserState
)瞧省,同時(shí)狀態(tài)是對(duì)界面的描述扯夭。于是我們?cè)诎咽录鳛檩斎耄?code>SetUserEvent),輸出狀態(tài)(SetUserState
)鞍匾,View只需要根據(jù)狀態(tài)SetUserState
的信息(如loading交洗,顯示錯(cuò)誤信息)來(lái)展示界面就可以了:
可以看到這是一條單向的“流”,而且是循環(huán)的橡淑,View把用戶事件輸出到Presenter构拳,接收狀態(tài)展示界面;Presenter對(duì)View的事件輸入進(jìn)行處理梁棠,輸出狀態(tài)置森。接下來(lái)看看如何用代碼實(shí)現(xiàn)。
首先定義界面狀態(tài)SetUserState
:
data class SetUserState(
val isLoading: Boolean, // 是否在加載
val isSuccess: Boolean, // 是否成功
val error: String? // 錯(cuò)誤信息
) {
companion object {
fun inProgress() = SetUserState(isLoading = true, isSuccess = false, error = null)
fun success() = SetUserState(isLoading = false, isSuccess = true, error = null)
fun failure(error: String) = SetUserState(isLoading = false, isSuccess = false, error = error)
}
}
這里定義了3個(gè)方法符糊,用于表示正在加載狀態(tài)凫海,成功狀態(tài)和失敗狀態(tài)。接下來(lái)對(duì)保存數(shù)據(jù)庫(kù)操作進(jìn)行重寫(xiě):
...
val setUserSubject = PublishSubject.create<SetUserEvent>()
init {
disposables.add(
setUserSubject.flatMap {
service.setUser(it.userName)
.map { SetUserState.success() }
.onErrorReturn { SetUserState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(SetUserState.inProgress())
}
.subscribe { setUserState ->
if (setUserState.isLoading) {
view.showLoading()
view.setButtonUnable()
return@subscribe
}
view.hideLoading()
view.setButtonEnable()
if (setUserState.isSuccess) {
// do something...
} else {
setUserState.error?.apply { view.showError(this) }
}
})
}
override fun setUser(setUserEvent: SetUserEvent) {
setUserSubject.onNext(setUserEvent)
}
修改的核心部分是flatMap
里的內(nèi)部Observable
:
service.setUser(it.userName)
.map { SetUserState.success() }
.onErrorReturn { SetUserState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(SetUserState.inProgress())
在這個(gè)內(nèi)部Observable
里濒蒋,把事件轉(zhuǎn)換為SetUserState
狀態(tài)并輸出盐碱。這個(gè)Observable
在執(zhí)行時(shí),會(huì)先輸出loading狀態(tài)(startWith(SetUserState.inProgress())
)沪伙;當(dāng)service.setUser(it.userName)
成功后輸出成功狀態(tài)(map { SetUserState.success() }
)瓮顽;當(dāng)錯(cuò)誤時(shí)輸出錯(cuò)誤狀態(tài),錯(cuò)誤狀態(tài)中包括錯(cuò)誤信息(onErrorReturn { SetUserState.failure(it.message.toString()) }
)围橡∨欤可以看到,我們不需要關(guān)心UI翁授,不需要關(guān)心什么時(shí)候調(diào)用view.showLoading()
顯示loading狀態(tài)拣播,不需要關(guān)心什么時(shí)候調(diào)用view.hideLoading()
隱藏loading狀態(tài),在subscribe()
中根據(jù)SetUserState
狀態(tài)展示界面就可以了收擦。為了方便單元測(cè)試和重用贮配,把這部分拆分出來(lái):
...
private val setUserTransformer = ObservableTransformer<SetUserEvent, SetUserState> {
event -> event.flatMap {
service.setUser(it.userName)
.map { SetUserState.success() }
.onErrorReturn { SetUserState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(SetUserState.inProgress())
}
}
init {
disposables.add(
setUserSubject.compose(setUserTransformer)
.subscribe { setUserState ->
if (setUserState.isLoading) {
view.showLoading()
view.setButtonUnable()
return@subscribe
}
view.hideLoading()
view.setButtonEnable()
if (setUserState.isSuccess) {
// do something...
} else {
setUserState.error?.apply { view.showError(this) }
}
})
}
...
一般情況下都會(huì)有很多輸入,如上拉加載下一頁(yè)塞赂,下拉刷新等±崂眨現(xiàn)假設(shè)需要添加一個(gè)checkUser()
方法,用于查詢用戶是否存在宴猾,要把不同輸入合并圆存,我們需要定義一個(gè)公共的父類UIEvent
,讓每個(gè)輸入都繼承該父類:
sealed class UIEvent {
data class SetUserEvent(val userName: String) : UIEvent()
data class CheckUserEvent(val userName: String) : UIEvent()
}
下面是Presenter的實(shí)現(xiàn):
class MainPresenter(
private val service: UserService,
private val view: MainView,
private val disposables: CompositeDisposable
) : Presenter {
val setUserSubject = PublishSubject.create<UIEvent.SetUserEvent>()
val checkUserSubject = PublishSubject.create<UIEvent.CheckUserEvent>()
private val setUserTransformer = ObservableTransformer<UIEvent.SetUserEvent, UIState> {
event -> event.flatMap {
service.setUser(it.userName)
.map { UIState.success() }
.onErrorReturn { UIState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(UIState.inProgress())
}
}
private val checkUserTransformer = ObservableTransformer<UIEvent.CheckUserEvent, UIState> {
event -> event.flatMap {
service.checkUser(it.userName)
.map { UIState.success() }
.onErrorReturn { UIState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(UIState.inProgress())
}
}
private val transformers = ObservableTransformer<UIEvent, UIState> {
events -> events.publish { shared ->
Observable.merge(
shared.ofType(UIEvent.SetUserEvent::class.java).compose(setUserTransformer),
shared.ofType(UIEvent.CheckUserEvent::class.java).compose(checkUserTransformer))
}
}
init {
val allEvents: Observable<UIEvent> = Observable.merge(setUserSubject, checkUserSubject)
disposables.add(
allEvents.compose(transformers)
.subscribe { setUserState ->
if (setUserState.isLoading) {
view.showLoading()
view.setButtonUnable()
return@subscribe
}
view.hideLoading()
view.setButtonEnable()
if (setUserState.isSuccess) {
// do something...
} else {
setUserState.error?.apply { view.showError(this) }
}
})
}
override fun setUser(setUserEvent: UIEvent.SetUserEvent) {
setUserSubject.onNext(setUserEvent)
}
override fun checkUser(checkUserEvent: UIEvent.CheckUserEvent) {
checkUserSubject.onNext(checkUserEvent)
}
}
如前面提到的仇哆,我們使用Observable.merge()
對(duì)輸入事件進(jìn)行合并:
val allEvents: Observable<UIEvent> = Observable.merge(setUserSubject, checkUserSubject)
然后按照前面的套路沦辙,定義checkUserTransformer
。這部分代碼需要注意的是transformers
屬性的實(shí)現(xiàn):
private val transformers = ObservableTransformer<UIEvent, UIState> {
events -> events.publish { shared ->
Observable.merge(
shared.ofType(UIEvent.SetUserEvent::class.java).compose(setUserTransformer),
shared.ofType(UIEvent.CheckUserEvent::class.java).compose(checkUserTransformer))
}
}
為了讓不同的事件輸入組合不同的業(yè)務(wù)邏輯讹剔,這里把合并的輸入拆分油讯,然后對(duì)不同的輸入組合不同的業(yè)務(wù)邏輯详民,最后再重新合并成一個(gè)流:
這樣做的好處是每個(gè)事件輸入做自己的事而不影響到其他。現(xiàn)在回過(guò)頭來(lái)整個(gè)流程撞羽,我們已經(jīng)實(shí)現(xiàn)了一個(gè)循環(huán)單向的流:
但細(xì)心的你會(huì)發(fā)現(xiàn)阐斜,左側(cè)邏輯部分跟View耦合了,事實(shí)上邏輯部分不應(yīng)該關(guān)心用戶的輸入事件(UIEvent
)是什么诀紊,也不應(yīng)該關(guān)心界面(UIState
)該怎么展示谒出,這還會(huì)導(dǎo)致該部分無(wú)法重用。為了把這部分解耦出來(lái)邻奠,我們多加一層轉(zhuǎn)換:
邏輯部分只關(guān)心Action
和Result
笤喳,不與View耦合。Result
并不關(guān)心界面狀態(tài)碌宴,只是某個(gè)Action
的結(jié)果杀狡,前面說(shuō)過(guò)狀態(tài)是對(duì)界面的描述,View根據(jù)狀態(tài)來(lái)展示相應(yīng)的界面贰镣,如果我們每次創(chuàng)建一個(gè)新的狀態(tài)就相當(dāng)于把界面重置了呜象,所以我們需要知道上一次的狀態(tài),來(lái)做相應(yīng)的調(diào)整碑隆,如開(kāi)始狀態(tài)UIState.isLoading = true
恭陡,成功后我們只需要UIState.isLoading = false
就可以了,借助RxJava的scan()
來(lái)實(shí)現(xiàn)這一點(diǎn):
sealed class Action {
data class SetUserAction(val userName: String) : Action()
data class CheckUserAction(val userName: String) : Action()
}
sealed class Result {
data class SetUserResult(
val isLoading: Boolean,
val isSuccess: Boolean,
val error: String?
) : Result() {
companion object {
fun inProgress() = SetUserResult(isLoading = true, isSuccess = false, error = null)
fun success() = SetUserResult(isLoading = false, isSuccess = true, error = null)
fun failure(error: String) = SetUserResult(
isLoading = false,
isSuccess = false,
error = error)
}
}
data class CheckNameResult(
val isLoading: Boolean,
val isSuccess: Boolean,
val error: String?
) : Result() {
companion object {
fun inProgress() = CheckNameResult(isLoading = true, isSuccess = false, error = null)
fun success() = CheckNameResult(isLoading = false, isSuccess = true, error = null)
fun failure(error: String) = CheckNameResult(
isLoading = false,
isSuccess = false,
error = error)
}
}
}
data class UIState(val isLoading: Boolean, val isSuccess: Boolean, val error: String?) {
companion object {
fun idle() = UIState(isLoading = false, isSuccess = false, error = null)
}
}
...
private val setUserTransformer = ObservableTransformer<Action.SetUserAction, Result.SetUserResult> {
event -> event.flatMap {
service.setUser(it.userName)
.map { Result.SetUserResult.success() }
.onErrorReturn { Result.SetUserResult.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(Result.SetUserResult.inProgress())
}
}
private val checkUserTransformer = ObservableTransformer<Action.CheckUserAction, Result.CheckNameResult> {
event -> event.flatMap {
service.checkUser(it.userName)
.map { Result.CheckNameResult.success() }
.onErrorReturn { Result.CheckNameResult.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(Result.CheckNameResult.inProgress())
}
}
private val transformers = ObservableTransformer<Action, Result> {
events -> events.publish { shared ->
Observable.merge(
shared.ofType(Action.SetUserAction::class.java).compose(setUserTransformer),
shared.ofType(Action.CheckUserAction::class.java).compose(checkUserTransformer))
}
}
init {
val setUserAction = setUserSubject.map { Action.SetUserAction(it.userName) }
val checkUserAction = checkUserSubject.map { Action.CheckUserAction(it.userName) }
val allActions: Observable<Action> = Observable.merge(setUserAction, checkUserAction)
disposables.add(
allActions.compose(transformers)
.scan(UIState.idle(),
{ previousState, result ->
when(result) {
is Result.SetUserResult -> {
previousState.copy(
isLoading = result.isLoading,
isSuccess = result.isSuccess,
error = result.error)
}
is Result.CheckNameResult -> {
previousState.copy(
isLoading = result.isLoading,
isSuccess = result.isSuccess,
error = result.error)
}
}
})
.subscribe { ... })
}
...
代碼比較多上煤,但邏輯應(yīng)該算比較清晰休玩,把setUserTransformer
及checkUserTransformer
屬性的輸入和輸出對(duì)象調(diào)整為Action
和Result
,在scan()
方法里根據(jù)上一次的狀態(tài)和當(dāng)前的結(jié)果Result
來(lái)組合新的狀態(tài)劫狠。
至此拴疤,我們簡(jiǎn)單的了解了狀態(tài)管理是如何實(shí)現(xiàn)的,接下來(lái)我們基于狀態(tài)管理的知識(shí)來(lái)講解MVI模式独泞。
MVI(Model-View-Intent)
什么是MVI
簡(jiǎn)單概括為:?jiǎn)蜗蛄?unidirectional flow)呐矾,數(shù)據(jù)流不可變(immutability)(關(guān)于不可變Model的優(yōu)缺點(diǎn)網(wǎng)上已經(jīng)很多,可自行百度或者查看該文章)懦砂,響應(yīng)式的凫佛,接收用戶輸入,通過(guò)函數(shù)轉(zhuǎn)換為特定Model(狀態(tài))孕惜,將其結(jié)果反饋給用戶(渲染界面)。把MVI抽象為model(), view(), intent()三個(gè)方法晨炕,描述如下:
- intent():中文意思為意圖衫画,將用戶操作(如觸摸,點(diǎn)擊瓮栗,滑動(dòng)等)作為數(shù)據(jù)流的輸入削罩,傳遞給model()方法瞄勾。
- model(): model()方法把intent()方法的輸出作為輸入來(lái)創(chuàng)建Model(狀態(tài)),傳遞給view()弥激。
- view(): view()方法把model()方法的輸出的Model(狀態(tài))作為輸入进陡,根據(jù)Model(狀態(tài))的結(jié)果來(lái)展示界面。
你會(huì)發(fā)現(xiàn)微服,這跟前面所說(shuō)的狀態(tài)管理描述的如出一轍趾疚,下面稍微詳細(xì)的描述一下MVI模式:
我們使用ViewModel來(lái)解耦業(yè)務(wù)邏輯,接收Intent(用戶意圖)并返回State(狀態(tài))以蕴,其中Processor用于處理業(yè)務(wù)邏輯糙麦,如前面的拆分出來(lái)setUserTransformer
和checkUserTransformer
屬性。
View只暴露2個(gè)方法:
interface MviView<I : MviIntent, in S : MviViewState> {
fun intents(): Observable<I>
fun render(state: S)
}
- 將用戶意圖傳遞給ViewModel
- 訂閱ViewModel輸出的狀態(tài)用于展示界面
同時(shí)ViewModel也只暴露2個(gè)方法:
interface MviViewModel<I : MviIntent, S : MviViewState> {
fun processIntents(intents: Observable<I>)
fun states(): Observable<S>
}
- 處理View傳遞過(guò)來(lái)的用戶意圖
- 輸出狀態(tài)給View丛肮,用于渲染界面
需要說(shuō)明的是赡磅,ViewModel會(huì)緩存最新的狀態(tài),當(dāng)Activity/Fragment
配置發(fā)生改變時(shí)(如屏幕旋轉(zhuǎn))宝与,我們不應(yīng)該重新創(chuàng)建
ViewModel焚廊,而是使用緩存的狀態(tài)來(lái)直接渲染界面,這里使用google的Architecture Components library的來(lái)實(shí)現(xiàn)ViewModel习劫,方便生命周期的管理咆瘟。
關(guān)于MVI的代碼實(shí)現(xiàn)可以參考狀態(tài)管理部分,下面是我寫(xiě)的demo中匯總頁(yè)的效果榜聂,這個(gè)頁(yè)面只有2個(gè)意圖搞疗,1)初始化意圖InitialIntent
,2)點(diǎn)擊曲線點(diǎn)切換月份意圖SwitchMonthIntent
须肆。
這里給出部分代碼實(shí)現(xiàn):
data class SummaryViewState(
val isLoading: Boolean, // 是否正在加載
val error: Throwable?, // 錯(cuò)誤信息
val points: List<Pair<Int, Float>>, // 曲線圖點(diǎn)
val months: List<Pair<String, Date>>, // 曲線圖月份
val values: List<String>, // 曲線圖數(shù)值文本
val selectedIndex: Int, // 曲線圖選中月份索引
val summaryItemList: List<SummaryListItem>, // 當(dāng)月標(biāo)簽匯總列表
val isSwitchMonth: Boolean // 是否切換月份
) : MviViewState {
companion object {
/**
* 初始[SummaryViewState]用于Reducer
*/
fun idle() = SummaryViewState(false, null, listOf(), listOf(), listOf(), 0, listOf(), false)
}
}
class SummaryActivity : BaseActivity(), MviView<SummaryIntent, SummaryViewState> {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var summaryViewModel: SummaryViewModel
private val disposables = CompositeDisposable()
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
bind()
}
private fun bind() {
summaryViewModel = ViewModelProviders.of(this, viewModelFactory)
.get(SummaryViewModel::class.java)
// 訂閱render方法根據(jù)發(fā)送過(guò)來(lái)的state渲染界面
disposables += summaryViewModel.states().subscribe(this::render)
// 傳遞UI的intents給ViewModel
summaryViewModel.processIntents(intents())
}
private fun initialIntent(): Observable<SummaryIntent> { ... }
private fun switchMonthIntent(): Observable<SummaryIntent> { ... }
override fun render(state: SummaryViewState) { ... }
override fun intents(): Observable<SummaryIntent> {
return Observable.merge(initialIntent(), switchMonthIntent())
}
...
}
class SummaryViewModel @Inject constructor(
private val summaryActionProcessorHolder: SummaryActionProcessorHolder
) : BaseViewModel<SummaryIntent, SummaryViewState>() {
override fun compose(intentsSubject: PublishSubject<SummaryIntent>):
Observable<SummaryViewState> =
intentsSubject
.compose(intentFilter)
.map(this::actionFromIntent)
.compose(summaryActionProcessorHolder.actionProcessor)
.scan(SummaryViewState.idle(), reducer)
.replay(1)
.autoConnect(0)
/**
* 只取一次初始化[MviIntent]和其他[MviIntent]匿乃,過(guò)濾掉配置改變(如屏幕旋轉(zhuǎn))后重新傳遞過(guò)來(lái)的初始化
* [MviIntent],導(dǎo)致重新加載數(shù)據(jù)
*/
private val intentFilter: ObservableTransformer<SummaryIntent, SummaryIntent> =
ObservableTransformer { intents -> intents.publish { shared ->
Observable.merge(
shared.ofType(SummaryIntent.InitialIntent::class.java).take(1),
shared.filter { it !is SummaryIntent.InitialIntent }
)
}
}
/**
* 把[MviIntent]轉(zhuǎn)換為[MviAction]
*/
private fun actionFromIntent(summaryIntent: SummaryIntent): SummaryAction =
when(summaryIntent) {
is SummaryIntent.InitialIntent -> {
SummaryAction.InitialAction()
}
is SummaryIntent.SwitchMonthIntent -> {
SummaryAction.SwitchMonthAction(summaryIntent.date)
}
}
private val reducer = BiFunction<SummaryViewState, SummaryResult, SummaryViewState> {
previousState, result ->
when(result) {
is SummaryResult.InitialResult -> {
when(result.status) {
LceStatus.SUCCESS -> {
previousState.copy(
isLoading = false,
error = null,
points = result.points,
months = result.months,
values = result.values,
selectedIndex = result.selectedIndex,
summaryItemList = result.summaryItemList,
isSwitchMonth = false)
}
LceStatus.FAILURE -> {
previousState.copy(isLoading = false, error = result.error)
}
LceStatus.IN_FLIGHT -> {
previousState.copy(isLoading = true, error = null)
}
}
}
is SummaryResult.SwitchMonthResult -> {
when(result.status) {
LceStatus.SUCCESS -> {
previousState.copy(
isLoading = false,
error = null,
summaryItemList = result.summaryItemList,
isSwitchMonth = true)
}
LceStatus.FAILURE -> {
previousState.copy(
isLoading = false,
error = result.error,
isSwitchMonth = true)
}
LceStatus.IN_FLIGHT -> {
previousState.copy(
isLoading = true,
error = null,
isSwitchMonth = true)
}
}
}
}
}
}
class SummaryActionProcessorHolder(
private val schedulerProvider: BaseSchedulerProvider,
private val applicationContext: Context,
private val accountingDao: AccountingDao) {
...
private val initialProcessor =
ObservableTransformer<SummaryAction.InitialAction, SummaryResult.InitialResult> {
actions -> actions.flatMap { ... }
}
private val switchMonthProcessor =
ObservableTransformer<SummaryAction.SwitchMonthAction, SummaryResult.SwitchMonthResult> {
actions -> actions.flatMap { ... }
}
/**
* 拆分[Observable<MviAction>]并且為不同的[MviAction]提供相應(yīng)的processor豌汇,processor用于處理業(yè)務(wù)邏輯幢炸,
* 同時(shí)把[MviAction]轉(zhuǎn)換為[MviResult],最終通過(guò)[Observable.merge]合并回一個(gè)流
*
* 為了防止遺漏[MviAction]未處理拒贱,在流的最后合并一個(gè)錯(cuò)誤檢測(cè)宛徊,方便維護(hù)
*/
val actionProcessor: ObservableTransformer<SummaryAction, SummaryResult> =
ObservableTransformer { actions -> actions.publish {
shared -> Observable.merge(
shared.ofType(SummaryAction.InitialAction::class.java)
.compose(initialProcessor),
shared.ofType(SummaryAction.SwitchMonthAction::class.java)
.compose(switchMonthProcessor))
.mergeWith(shared.filter {
it !is SummaryAction.InitialAction &&
it !is SummaryAction.SwitchMonthAction
}
.flatMap {
Observable.error<SummaryResult>(
IllegalArgumentException("Unknown Action type: $it"))
})
}
}
}
這里不帖過(guò)多的代碼了,感興趣的兄弟可以查看我寫(xiě)的demo(一個(gè)簡(jiǎn)單的增刪改記帳app)逻澳,演示了如何用狀態(tài)管理的方式實(shí)現(xiàn)MVI闸天,邏輯比較簡(jiǎn)單。
測(cè)試
編寫(xiě)單元測(cè)試的時(shí)候斜做,我們只需要提供用戶意圖苞氮,借助RxJava的TestObserver
,測(cè)試輸出的狀態(tài)是否符合我們預(yù)期的狀態(tài)就可以了瓤逼,如下面代碼片段:
summaryViewModel.processIntents(SummaryIntent.InitialIntent())
testObserver.assertValueAt(2, SummaryViewState(...))
這消除了很多我們用MVP時(shí)對(duì)View的驗(yàn)證測(cè)試笼吟,如Mockito.verify(view库物,times(1)).showFoo()
,因?yàn)槲覀儾槐靥幚韺?shí)際代碼的實(shí)現(xiàn)細(xì)節(jié)贷帮,使得單元測(cè)試的代碼更具可讀性戚揭,可理解性和可維護(hù)性∧焓啵總所周知民晒,在Android中UI測(cè)試是一件很頭大的事,但狀態(tài)是界面的描述诲侮,按照狀態(tài)來(lái)展示界面镀虐,對(duì)界面顯示正確性也有所幫助,但是要保證界面顯示正確性沟绪,還是需要編寫(xiě)UI測(cè)試代碼刮便。
總結(jié)
文章花了很大的篇幅介紹狀態(tài)管理(其實(shí)就是代碼比較多),因?yàn)闋顟B(tài)管理理解了绽慈,MVI也理解了恨旱。強(qiáng)烈建議大家看下Jake Wharton關(guān)于狀態(tài)管理的演講(youtube),和Hannes Dorfmann’s 關(guān)于MVI的系列博客坝疼。感謝您閱讀本文搜贤,希望對(duì)您有幫助。本文的demo 已上傳到github钝凶,如果對(duì)本文有疑問(wèn)仪芒,或者哪里說(shuō)得不對(duì)的地方,歡迎在github上實(shí)錘耕陷。
參考
Managing State with RxJava by Jake Wharton
github TODO-MVI-RxJava
REACTIVE APPS WITH MODEL-VIEW-INTENT PART 1 - 7