本文同步自我的博客從狀態管理(State Manage)到MVI(Model-View-Intent)html
什麼是狀態?界面上展現給用戶的都是一種狀態,如loading顯示,error信息顯示,列表展現等。這是平常開發中必然會遇到的,本文將講解如何用更有效的方式來進行狀態管理,提升代碼的可讀性,可維護性,健壯性。。。。。。文章中代碼示例比較多,可是別慌,邏輯都比較簡單,穩住就行。文章代碼使用kotlin實現,關於狀態管理部分示例代碼使用MVP
+ RxJava
模式來編寫。java
假設咱們有這樣一個需求:在輸入框輸入用戶名,點擊保存按鈕把用戶保存到數據庫。在保存數據庫以前,顯示loading狀態,而後把保存按鈕設置爲不可點擊,保存數據庫須要異步操做,最後在成功的時候隱藏loading狀態而且把保存按鈕設置爲可點擊,若發生錯誤,須要隱藏loading狀態,把保存按鈕設置爲可點擊狀態,而後顯示錯誤信息。Show you the code:android
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)
}
}
複製代碼
這段代碼看上去不怎麼優雅,但已經實現了咱們的需求了。簡單畫下流程圖: git
能夠看到當保存數據庫操做前調用view.showLoading()
和view.setButtonUnable()
,當操做成功或者錯誤的時候調用view.hideLoading()
和view.setButtonEnable()
,像這種「配套」方法愈來愈多的時候就很容易會疏忽,出現忘記隱藏loading狀態,忘記把按鈕設置爲可點擊等問題。在這簡單例子你可能會以爲沒什麼,實際開發的時候必定會記得調用相應的方法,這不一樣於註冊接口監聽,通常咱們會在Activity#onCreate()
的時候註冊監聽,在Activity#onDestroy()
取消監聽,但咱們在View裏能夠有不少地方調用Presenter的方法,如setUser()
,咱們認爲調用Presenter方法是一種輸入,同時Presenter也有不少地方輸出狀態給View,如view.showLoading()
,view.showError()
等。咱們不能肯定setUser()
方法在哪裏被調用,view.showLoading()
方法在哪裏被調用,假設咱們還有其餘方法在同時執行: github
這很容易會形成狀態混亂,例如loading狀態和錯誤信息同時出現,當錯誤信息顯示的時候保存按鈕沒有恢復可點擊狀態等,在實際的業務中,這種問題尤爲明顯。數據庫
咱們能不能限制Presenter只有一個輸入,狀態只從一個地方輸出呢?咱們藉助PublishSubject
做爲橋接(如上面代碼片斷setUserSubject
),而後經過Observable.merge()
把它們合併成一個流,來實現只有一個地方輸入。下面咱們主要看看咱們如何實現狀態只從一個地方輸出。編程
引用面向對象編程一句經典的話:萬物皆對象。用戶輸入用戶名,點擊保存按鈕,這是一個事件,咱們把它當作一個事件對象SetUserEvent
,把UI狀態做爲一個狀態對象(SetUserState
),同時狀態是對界面的描述。因而咱們在把事件做爲輸入(SetUserEvent
),輸出狀態(SetUserState
),View只須要根據狀態SetUserState
的信息(如loading,顯示錯誤信息)來展現界面就能夠了: 緩存
能夠看到這是一條單向的「流」,並且是循環的,View把用戶事件輸出到Presenter,接收狀態展現界面;Presenter對View的事件輸入進行處理,輸出狀態。接下來看看如何用代碼實現。app
首先定義界面狀態SetUserState
:異步
data class SetUserState(
val isLoading: Boolean, // 是否在加載
val isSuccess: Boolean, // 是否成功
val error: String? // 錯誤信息
) {
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個方法,用於表示正在加載狀態,成功狀態和失敗狀態。接下來對保存數據庫操做進行重寫:
...
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
裏的內部Observable
:
service.setUser(it.userName)
.map { SetUserState.success() }
.onErrorReturn { SetUserState.failure(it.message.toString()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith(SetUserState.inProgress())
複製代碼
在這個內部Observable
裏,把事件轉換爲SetUserState
狀態並輸出。這個Observable
在執行時,會先輸出loading狀態(startWith(SetUserState.inProgress())
);當service.setUser(it.userName)
成功後輸出成功狀態(map { SetUserState.success() }
);當錯誤時輸出錯誤狀態,錯誤狀態中包括錯誤信息(onErrorReturn { SetUserState.failure(it.message.toString()) }
)。能夠看到,咱們不須要關心UI,不須要關心何時調用view.showLoading()
顯示loading狀態,不須要關心何時調用view.hideLoading()
隱藏loading狀態,在subscribe()
中根據SetUserState
狀態展現界面就能夠了。爲了方便單元測試和重用,把這部分拆分出來:
...
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) }
}
})
}
...
複製代碼
通常狀況下都會有不少輸入,如上拉加載下一頁,下拉刷新等。現假設須要添加一個checkUser()
方法,用於查詢用戶是否存在,要把不一樣輸入合併,咱們須要定義一個公共的父類UIEvent
,讓每一個輸入都繼承該父類:
sealed class UIEvent {
data class SetUserEvent(val userName: String) : UIEvent()
data class CheckUserEvent(val userName: String) : UIEvent()
}
複製代碼
下面是Presenter的實現:
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()
對輸入事件進行合併:
val allEvents: Observable<UIEvent> = Observable.merge(setUserSubject, checkUserSubject)
複製代碼
而後按照前面的套路,定義checkUserTransformer
。這部分代碼須要注意的是transformers
屬性的實現:
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))
}
}
複製代碼
爲了讓不一樣的事件輸入組合不一樣的業務邏輯,這裏把合併的輸入拆分,而後對不一樣的輸入組合不一樣的業務邏輯,最後再從新合併成一個流:
這樣作的好處是每一個事件輸入作本身的事而不影響到其餘。如今回過頭來整個流程,咱們已經實現了一個循環單向的流:
但細心的你會發現,左側邏輯部分跟View耦合了,事實上邏輯部分不該該關心用戶的輸入事件(UIEvent
)是什麼,也不該該關心界面(UIState
)該怎麼展現,這還會致使該部分沒法重用。爲了把這部分解耦出來,咱們多加一層轉換:
邏輯部分只關心Action
和Result
,不與View耦合。Result
並不關心界面狀態,只是某個Action
的結果,前面說過狀態是對界面的描述,View根據狀態來展現相應的界面,若是咱們每次建立一個新的狀態就至關於把界面重置了,因此咱們須要知道上一次的狀態,來作相應的調整,如開始狀態UIState.isLoading = true
,成功後咱們只須要UIState.isLoading = false
就能夠了,藉助RxJava的scan()
來實現這一點:
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 { ... })
}
...
複製代碼
代碼比較多,但邏輯應該算比較清晰,把setUserTransformer
及checkUserTransformer
屬性的輸入和輸出對象調整爲Action
和Result
,在scan()
方法里根據上一次的狀態和當前的結果Result
來組合新的狀態。
至此,咱們簡單的瞭解了狀態管理是如何實現的,接下來咱們基於狀態管理的知識來說解MVI模式。
簡單歸納爲:單向流(unidirectional flow),數據流不可變(immutability)(關於不可變Model的優缺點網上已經不少,可自行百度或者查看該文章),響應式的,接收用戶輸入,經過函數轉換爲特定Model(狀態),將其結果反饋給用戶(渲染界面)。把MVI抽象爲model(), view(), **intent()**三個方法,描述以下:
你會發現,這跟前面所說的狀態管理描述的一模一樣,下面稍微詳細的描述一下MVI模式:
咱們使用ViewModel來解耦業務邏輯,接收Intent(用戶意圖)並返回State(狀態),其中Processor用於處理業務邏輯,如前面的拆分出來setUserTransformer
和checkUserTransformer
屬性。 View只暴露2個方法:
interface MviView<I : MviIntent, in S : MviViewState> {
fun intents(): Observable<I>
fun render(state: S)
}
複製代碼
同時ViewModel也只暴露2個方法:
interface MviViewModel<I : MviIntent, S : MviViewState> {
fun processIntents(intents: Observable<I>)
fun states(): Observable<S>
}
複製代碼
須要說明的是,ViewModel會緩存最新的狀態,當Activity/Fragment
配置發生改變時(如屏幕旋轉),咱們不該該從新建立 ViewModel,而是使用緩存的狀態來直接渲染界面,這裏使用google的Architecture Components library的來實現ViewModel,方便生命週期的管理。
關於MVI的代碼實現能夠參考狀態管理部分,下面是我寫的demo中彙總頁的效果,這個頁面只有2個意圖,1)初始化意圖InitialIntent
,2)點擊曲線點切換月份意圖SwitchMonthIntent
。
這裏給出部分代碼實現:
data class SummaryViewState(
val isLoading: Boolean, // 是否正在加載
val error: Throwable?, // 錯誤信息
val points: List<Pair<Int, Float>>, // 曲線圖點
val months: List<Pair<String, Date>>, // 曲線圖月份
val values: List<String>, // 曲線圖數值文本
val selectedIndex: Int, // 曲線圖選中月份索引
val summaryItemList: List<SummaryListItem>, // 當月標籤彙總列表
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方法根據發送過來的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],過濾掉配置改變(如屏幕旋轉)後從新傳遞過來的初始化 * [MviIntent],致使從新加載數據 */
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]轉換爲[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]提供相應的processor,processor用於處理業務邏輯, * 同時把[MviAction]轉換爲[MviResult],最終經過[Observable.merge]合併回一個流 * * 爲了防止遺漏[MviAction]未處理,在流的最後合併一個錯誤檢測,方便維護 */
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"))
})
}
}
}
複製代碼
這裏不帖過多的代碼了,感興趣的兄弟能夠查看我寫的demo(一個簡單的增刪改記賬app),演示瞭如何用狀態管理的方式實現MVI,邏輯比較簡單。
編寫單元測試的時候,咱們只須要提供用戶意圖,藉助RxJava的TestObserver
,測試輸出的狀態是否符合咱們預期的狀態就能夠了,以下面代碼片斷:
summaryViewModel.processIntents(SummaryIntent.InitialIntent())
testObserver.assertValueAt(2, SummaryViewState(...))
複製代碼
這消除了不少咱們用MVP時對View的驗證測試,如Mockito.verify(view,times(1)).showFoo()
,由於咱們沒必要處理實際代碼的實現細節,使得單元測試的代碼更具可讀性,可理解性和可維護性。總所周知,在Android中UI測試是一件很頭大的事,但狀態是界面的描述,按照狀態來展現界面,對界面顯示正確性也有所幫助,可是要保證界面顯示正確性,仍是須要編寫UI測試代碼。
文章花了很大的篇幅介紹狀態管理(其實就是代碼比較多),由於狀態管理理解了,MVI也理解了。強烈建議你們看下Jake Wharton關於狀態管理的演講(youtube),和Hannes Dorfmann’s 關於MVI的系列博客。感謝您閱讀本文,但願對您有幫助。本文的demo 已上傳到github,若是對本文有疑問,或者哪裏說得不對的地方,歡迎在github上實錘。
Managing State with RxJava by Jake Wharton
github TODO-MVI-RxJava
REACTIVE APPS WITH MODEL-VIEW-INTENT PART 1 - 7