從狀態(tài)管理(State Manage)到MVI(Model-View-Intent)

文章同步于掘金從狀態(tài)管理(State Manage)到MVI(Model-View-Intent)燎悍。

什么是狀態(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à)下流程圖:


setUser流程

可以看到當(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í)行:

多個(gè)方法調(diào)用流程

這很容易會(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)展示界面就可以了:

用戶輸入-狀態(tà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è)流:


publish拆分-merge合并

這樣做的好處是每個(gè)事件輸入做自己的事而不影響到其他。現(xiàn)在回過(guò)頭來(lái)整個(gè)流程撞羽,我們已經(jīng)實(shí)現(xiàn)了一個(gè)循環(huán)單向的流:

用戶輸入-狀態(tài)

但細(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)換:

增加Action-Result轉(zhuǎn)換

邏輯部分只關(guān)心ActionResult笤喳,不與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)該算比較清晰休玩,把setUserTransformercheckUserTransformer屬性的輸入和輸出對(duì)象調(diào)整為ActionResult,在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è)方法晨炕,描述如下:

MVI示意圖
  • 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模式:

mvi-detail

我們使用ViewModel來(lái)解耦業(yè)務(wù)邏輯,接收Intent(用戶意圖)并返回State(狀態(tài))以蕴,其中Processor用于處理業(yè)務(wù)邏輯糙麦,如前面的拆分出來(lái)setUserTransformercheckUserTransformer屬性。
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须肆。

SummaryActivity

這里給出部分代碼實(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掂名,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子哟沫,更是在濱河造成了極大的恐慌饺蔑,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗜诀,死亡現(xiàn)場(chǎng)離奇詭異猾警,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)隆敢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)发皿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拂蝎,你說(shuō)我怎么就攤上這事雳窟。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵封救,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捣作,道長(zhǎng)誉结,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任券躁,我火速辦了婚禮惩坑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘也拜。我一直安慰自己以舒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布慢哈。 她就那樣靜靜地躺著蔓钟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卵贱。 梳的紋絲不亂的頭發(fā)上滥沫,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音键俱,去河邊找鬼兰绣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛编振,可吹牛的內(nèi)容都是我干的缀辩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼踪央,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼臀玄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起杯瞻,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤镐牺,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后魁莉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體睬涧,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年旗唁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畦浓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡检疫,死狀恐怖讶请,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤夺溢,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布论巍,位于F島的核電站,受9級(jí)特大地震影響风响,放射性物質(zhì)發(fā)生泄漏嘉汰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一状勤、第九天 我趴在偏房一處隱蔽的房頂上張望鞋怀。 院中可真熱鬧,春花似錦持搜、人聲如沸密似。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)残腌。三九已至,卻和暖如春剪返,著一層夾襖步出監(jiān)牢的瞬間废累,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工脱盲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留邑滨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓钱反,卻偏偏與公主長(zhǎng)得像掖看,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子面哥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353