前言
Android真響應式架構系列文章:
Android真響應式開發——MvRx
Epoxy——RecyclerView的絕佳助手
Android真響應式架構——Model層設計
Android真響應式架構——數據流動性
Android真響應式架構——Epoxy的使用
Android真響應式架構——MvRx和Epoxy的結合
Android單向數據流——MvRx核心源碼解析
Airbnb 最近開源了一個庫,他們稱之為Android界的Autopilot——MvRx(ModelView ReactiveX的縮寫,讀作mavericks)。這個庫并不“單純”,它其實是一個架構,已經被應用在了Airbnb幾乎所有的產品上。
這個庫綜合運用了以下幾種技術
- Kotlin (MvRx is Kotlin first and Kotlin only)
- Android Architecture Components
- RxJava
- React (概念上的)
- Epoxy (可選但推薦)
光看這個清單,也知道事情并不簡單。利用這個庫我們可以方便地構建出MVVM架構的APP,讓開發更加的簡單、高效。
1. 真響應式架構
響應式(React)架構并沒有什么定義,只是我覺得這么描述MvRx比較準確。這里所說的響應式架構是指,數據響應式以及界面響應式。數據響應式大體指數據以流的形式呈現(RxJava那套東西),界面響應式大體指數據驅動界面更新,界面顯示與數據狀態保持一致。
以如上的定義來看,在RxJava的幫助下,幾乎所有架構都可以實現數據響應式,因為數據響應式實際上是Model層的設計。但是界面響應式則基本上沒有哪個框架實現了,最接近的應該是Android Architecture Components,但是Android Architecture Components并沒有保證界面與數據狀態的一致,我們通過LiveData通知界面更新,只是把數據帶給了界面,界面顯示與數據狀態并不一定是一致的(例如,LiveData攜帶了下一頁的數據,界面只是把該數據加到了RecyclerView的后面,數據并沒有完全代表了當前界面的狀態)。而MvRx真正實現了界面的響應式,所以我稱之為真響應式架構。
如果你了解過Flutter,那么MvRx很容易理解,因為兩者都采用了響應式構建的思想,以下是關于Flutter的描述,把它替換為MvRx也基本上適用。
Flutter 組件采用現代響應式框架構建,這是從 React 中獲得的靈感,中心思想是用組件 (widget) 構建你的 UI。 組件描述了在給定其當前配置和狀態時他們顯示的樣子。當組件狀態改變,組件會重構它的描述 (description),Flutter 會對比之前的描述,以確定底層渲染樹從當前狀態轉換到下一個狀態所需要的最小更改。
由于Flutter的實現不受原生的限制,它完全用另外一套方式實現了界面的渲染,并且響應式在設計之初就是Flutter的核心,所以在Flutter中任何組件(可以理解為Android中的View)都是響應式的,都可以確定它從當前狀態轉換到下一個狀態所需要的最小更改,顯然這一點在原生Android上是實現不了的。而MvRx在原生Android的基礎上幾乎實現了所有界面的響應式,這一點還是非常厲害的。
1.1 命令式MVP與響應式MVVM
MVP模式在Android界一直很流行,因為它比較好理解。其核心思想是,通過接口隔離數據與顯示,數據的變動通過接口回調的方式去通知界面更新。這正是典型的命令式M-V(數據-顯示)鏈接。在這種模式下View層是完全被動的,完全受控于Presenter層的命令。這種模式并沒有什么大問題,只是有一些不太方便之處,主要體現在M-V的緊密鏈接,導致復用比較困難,要么View層需要定義不必要的接口(這樣Presenter可以復用),要么就需要為幾乎每個View都定義一個對應的Presenter,想想都心累。
不同于MVP通過接口的方式來隔離數據與顯示,MVVM是使用觀察者的方式來隔離數據與顯示。以Android Architecture Components構建的MVVM模式為例,View通過觀察LiveData來驅動界面更新。MVVM帶來的主要好處是打破了M-V的緊密鏈接,ViewModel復用變得很簡單,View層需要什么數據觀察什么數據即可。將View抽離為觀察者,可以實現響應式MVVM架構,只是View本身不是響應式的。
以我的實踐來看Android Architecture Components構建的MVVM的主要問題是,RxJava與LiveData的銜接并不方便,還有就是按照Google給出的sample,數據加載的狀態需要和數據本身打包在一起,然后通過LiveData傳遞出去,我覺得這不是一個好的做法。我在實踐中是在Observer的onSubscribe,onNext,onError方法中分別對不同的MutableLiveData賦值,然后在View中去觀察這些LiveData來更新界面的。說實話,這很丑陋,但是比Google給出的sample要方便許多。
1.2 MvRx的真響應式MVVM
MvRx構建的MVVM模式,完美地解決了上述的問題。MvRx放棄了LiveData,使用State來通知View層數據的改變(當然仍然是可感知生命周期的)。MvRx可以方便地把RxJava Observable的請求過程包裝成Ansyc類,不僅可以改變State來通知View層,而且也包含了數據加載的狀態(成功、失敗、加載中等)。如果結合Airbnb的另一個開源庫Epoxy,那么幾乎可以做到真正的響應式,即View層在數據改變時僅僅描述當前數據狀態下界面的樣子,Epoxy可以幫我們實現與之前數據狀態的比較,然后找出差別,僅更新那些有差別的View部分。這是對MvRx的大致描述。下面來看看MvRx是如果使用的。
2. MvRx的使用
2.1 MvRx的重要概念
MvRx有四個重要的概念,分別是State、ViewModel、View和Async。
State
包含界面顯示的所有數據,實現類需是繼承自MvRxState
的immutable Kotlin data class。像是這樣
data class TasksState(
val tasks: List<Task> = emptyList(),
val taskRequest: Async<List<Task>> = Uninitialized,
val isLoading: Boolean = false,
val lastEditedTask: String? = null
) : MvRxState //MvRxState 僅是一個標記接口
State的作用是承載數據,并且應該包含有界面顯示的所有數據。當然可以對界面進行拆分,使用多個State共同決定界面的顯示。
State必須是不可變的(immutable),即State的所有屬性必須是val
的。只有ViewModel可以改變State,改變State時一般使用其copy
方法,創建一個新的State對象。
可以把MvRx的State類比成Architecture Components中的LiveData,它們的相同點是都可以被View觀察,不同點是,State的改變會觸發View的invalidate()
方法,從而通知界面重繪。
ViewModel
完全繼承自Architecture Components中的ViewModel,ViewModel包含有除了界面顯示之外的業務邏輯。此外,最關鍵的一點是,ViewModel還包含有一個State,ViewModel可以改變State的狀態,然后View可以觀察State的狀態。實現類需繼承BaseMvRxViewModel
,并且必須向BaseMvRxViewModel
傳遞initialState
(代表了View的初始狀態)。像是這樣
class TasksViewModel(initialState: TasksState) : BaseMvRxViewModel<TasksState>(initialState)
View
一般而言是一個繼承自BaseMvRxFragment
的Fragment。BaseMvRxFragment
實現了接口MvRxView
,這個接口有一個invalidate()
方法,每當ViewModel的state發生改變時invalidate()
方法都會被調用。View也可以觀察State中的某個或某幾個屬性的變化,View是沒辦法改變State狀態的,只有ViewModel可以改變State的狀態。
Async
代表了數據加載的狀態。Async
是一個Kotlin sealed class,它有四種類型:Uninitialized
, Loading
, Success
, Fail
(包含了一個名為error
的屬性,可以獲取錯誤類型)。Async
重載了操作符invoke
,除了在Success
返回數據外,其它情況下都返回null:
var foo = Loading()
println(foo()) // null
foo = Success<Int>(5)
println(foo()) // 5
foo = Fail(IllegalStateException("bar"))
println(foo()) // null
在ViewModel中可以通過擴展函數execute
把Observable<T>
的請求過程包裝成Asnyc<T>
,這可以方便地表示數據獲取的狀態(下面會有介紹)。
以上四個核心概念是怎么聯系到一起的呢?請看下圖:
圖中沒有包含Asnyc
,State
可包含若干個Asnyc
,用來表示數據加載的狀態,便于顯示Loading或者加載錯誤信息等。
按照理想情形,View不需要主動觀察State,State的任意改變都會調用View的invalidate方法,在invalidate方法中根據當前的State(在View中通過ViewModel的withState方法獲取State)直接重繪一下View即可。然而這太過于理想,實際上可以通過selectSubscribe,asyncSubscribe等方法觀察State中某個屬性的改變,根據特定的屬性更新View的特定部分。
以上是MvRx的四個核心概念。下面以官方sample為例,展示一下MvRx應該怎樣使用。
2.2 如何使用
ToDo Sample,架構界的Hello World。界面張這個樣子。
以下以首界面為例,介紹應該如何使用MvRx。
2.2.1 State的使用
//待辦事的定義,包含有id, title, description以及是否完成標志complete
data class Task(
var title: String = "",
var description: String = "",
var id: String = UUID.randomUUID().toString(),
var complete: Boolean = false
)
data class TasksState(
val tasks: List<Task> = emptyList(), //界面上的待辦事
val taskRequest: Async<List<Task>> = Uninitialized, //代表請求的狀態
val isLoading: Boolean = false, //是否顯示Loading
val lastEditedTask: String? = null //上次編輯的待辦事ID
) : MvRxState
State包含了這個界面要顯示的所有數據。
2.2.2 ViewModel的使用
具體的業務邏輯并不重要,主要看ViewModel是如何定義的。
/**
* 必須有一個initialState
* source是數據源,可以是數據庫,也可以是網絡請求等(例子中是數據庫)
**/
class TasksViewModel(initialState: TasksState, private val source: TasksDataSource) : MvRxViewModel<TasksState>(initialState) {
//工廠方法,必須實現MvRxViewModelFactory接口
companion object : MvRxViewModelFactory<TasksViewModel, TasksState> {
/**
* 主要用途是通過依賴注入傳入一些參數來構造ViewModel
* TasksState是MvRx幫我們構造的(通過反射)
**/
override fun create(viewModelContext: ViewModelContext, state: TasksState): BaseMvRxViewModel<TasksState> {
//例子中并沒有使用依賴注入,而是直接獲取數據庫
val database = ToDoDatabase.getInstance(viewModelContext.activity)
val dataSource = DatabaseDataSource(database.taskDao(), 2000)
return TasksViewModel(state, dataSource)
}
}
init {
//方便調試,State狀態改變時打印出來
logStateChanges()
//初始加載任務
refreshTasks()
}
//獲取待辦事
fun refreshTasks() {
source.getTasks()
.doOnSubscribe { setState { copy(isLoading = true) } }
.doOnComplete { setState { copy(isLoading = false) } }
//execute把Observable包裝成Async
.execute { copy(taskRequest = it, tasks = it() ?: tasks, lastEditedTask = null) }
}
//新增或者更新待辦事
fun upsertTask(task: Task) {
//通過setState改變 State的狀態
setState { copy(tasks = tasks.upsert(task) { it.id == task.id }, lastEditedTask = task.id) }
//因為是數據庫操作,一般不會失敗,所以沒有理會數據操作的狀態
source.upsertTask(task)
}
//標記任務完成與否
fun setComplete(id: String, complete: Boolean) {
setState {
//沒有這個任務,拉倒;this指之前的 State,直接返回之前的 State意思就是無需更新
val task = tasks.findTask(id) ?: return@setState this
//這個任務已經完成了,拉倒
if (task.complete == complete) return@setState this
//找到這個任務,并更新
copy(tasks = tasks.copy(tasks.indexOf(task), task.copy(complete = complete)), lastEditedTask = id)
}
//數據庫更新
source.setComplete(id, complete)
}
//清空已完成的待辦事
fun clearCompletedTasks() = setState {
source.clearCompletedTasks()
copy(tasks = tasks.filter { !it.complete }, lastEditedTask = null)
}
//刪除待辦事
fun deleteTask(id: String) {
setState { copy(tasks = tasks.delete { it.id == id }, lastEditedTask = id) }
source.deleteTask(id)
}
}
ViewModel實現了業務邏輯,其核心作用就是與Model層(這里的source)溝通,并更新State。這里有幾點需要說明:
- 按照MvRx的要求,ViewModel可以沒有工廠方法,這樣的話MvRx會通過反射構造出ViewModel(當然這一般不可能,畢竟ViewModel一般都包含Model層)。如果ViewModel包含有除initialState之外的其它構造參數,則需要我們實現工廠方法。如上所示,必須通過伴生對象實現
MvRxViewModelFactory
接口。 - 只能在ViewModel中更新State。更新State有兩種方法,
setState
或者execute
。setState
很好理解,直接更新State即可。其定義如下
abstract class BaseMvRxViewModel<S : MvRxState> {
//參數是State上的擴展函數,會接收到上次 State的值
protected fun setState(reducer: S.() -> S) {
//...
}
}
因為State是immutable Kotlin data class,所以一般而言都是通過data class的copy
方法返回新的State。execute
是一個擴展方法,其定義如下
abstract class BaseMvRxViewModel<S : MvRxState> {
/**
* Helper to map an observable to an Async property on the state object.
*/
//參數依然是State上的擴展函數
fun <T> Observable<T>.execute(
stateReducer: S.(Async<T>) -> S
) = execute({ it }, null, stateReducer)
/**
* Execute an observable and wrap its progression with AsyncData reduced to the global state.
*
* @param mapper A map converting the observable type to the desired AsyncData type.
* @param successMetaData A map that provides metadata to set on the Success result.
* It allows data about the original Observable to be kept and accessed later. For example,
* your mapper could map a network request to just the data your UI needs, but your base layers could
* keep metadata about the request, like timing, for logging.
* @param stateReducer A reducer that is applied to the current state and should return the
* new state. Because the state is the receiver and it likely a data
* class, an implementation may look like: `{ copy(response = it) }`.
*
* @see Success.metadata
*/
fun <T, V> Observable<T>.execute(
mapper: (T) -> V,
successMetaData: ((T) -> Any)? = null,
stateReducer: S.(Async<V>) -> S
): Disposable {
setState { stateReducer(Loading()) }
return map {
val success = Success(mapper(it))
success.metadata = successMetaData?.invoke(it)
success as Async<V>
}
.onErrorReturn { Fail(it) }
.subscribe { asyncData -> setState { stateReducer(asyncData) } }
.disposeOnClear() //ViewModel clear的時候dispose
}
}
execute
方法可以把Observable
的請求過程包裝成Async
,我們都知道訂閱Observable
需要有onNext
,onComplete
,onError
等方法,execute
就是把這些個方法包裝成了統一的Async
類。前面已經說過,Async
是sealed class,只有四個子類:Uninitialized
, Loading
, Success
, Fail
。這些子類完美的描述了一次請求的過程,并且它們重載了invoke
操作符(Success
情況下返回請求的數據,其它情況均為null
)。因此經常看到這樣的樣板代碼:
fun <T> Observable<T>.execute(
stateReducer: S.(Async<T>) -> S
)
/**
* 根據上面execute的定義,我們傳遞過去的是State上的以Async<T>為參數的擴展函數
* 因此下面的it參數是指 Async<T>,it()是獲取請求的結果,tasks = it() ?: tasks 表示只在請求 Success時更新State
**/
fun refreshTasks() {
source.getTasks()
//...
.execute { copy(taskRequest = it, tasks = it() ?: tasks, lastEditedTask = null) }
}
2.2.3 View的使用
abstract class BaseFragment : BaseMvRxFragment() {
//activityViewModel是MvRx定義的獲取ViewModel的方式
//按照規范必須使用activityViewModel、fragmentViewModel、existingViewModel(都是Lazy<T>類)獲取ViewModel
protected val viewModel by activityViewModel(TasksViewModel::class)
//Epoxy的使用
protected val epoxyController by lazy { epoxyController() }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//可以觀察State中某個(某幾個)屬性的變化
viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
//...
}
//觀察Async屬性
viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
})
}
//State的改變均會觸發
override fun invalidate() {
//Epoxy的用法
recyclerView.requestModelBuild()
}
abstract fun epoxyController(): ToDoEpoxyController
}
class TaskListFragment : BaseFragment() {
//另一個ViewModel
private val taskListViewModel: TaskListViewModel by fragmentViewModel()
//Epoxy的使用
override fun epoxyController() = simpleController(viewModel, taskListViewModel) { state, taskListState ->
// We always want to show this so the content won't snap up when the loader finishes.
horizontalLoader {
id("loader")
loading(state.isLoading)
}
//...
}
}
按照MvRx的規范,View通過activityViewModel
(ViewModel被置于Activity中), fragmentViewModel
(ViewModel被置于Fragment中), existingViewModel
(從Activity中獲取已存在的ViewModel) 來獲取ViewModel,這是因為,以這幾種方式獲取ViewModel,MvRx會幫我們完成如下幾件事:
-
activityViewModel
,fragmentViewModel
,existingViewModel
其實都是Kotlin的Lazy
子類,顯然會是懶加載。但是它不是真正的“懶”,因為在這些子類的構造函數中會添加一個對View生命周期的觀察者,在ON_CREATE
事件發生時會構造出ViewModel,也就是說ViewModel最晚到ON_CREATE
時即被構造完成(為了及早發出網絡請求等)。 - 通過反射構造出State,ViewModel。
- 調用ViewModel的
subscribe
方法,觀察State的改變,如果改變則調用View的invalidate
方法。
當State發生改變時,View的invalidate
方法會被調用。invalidate
被調用僅說明了State發生了改變,究竟是哪個屬性發生的改變并不得而知,按照MvRx的“理想”,哪個屬性發生改變并不重要,只要View根據當前的State“重繪”一下View即可。這里“重繪”顯然指的不是簡單地重繪整個界面,應該是根據當前State“描繪”當前界面,然后與上次界面作比較,只更新差異部分。顯然這種“理想”太過于高級,需要有一個幫手來完成這項任務,于是就有了Epoxy(其實是先有的Epoxy)。
Epoxy簡單來說就是RecyclerView的高級助手,我們只需要定義某個數據在RecyclerView的ItemView上是如何顯示的,然后把一堆數據扔給Epoxy就行了。Epoxy會幫我們分析這次的數據跟上次的數據有什么差別,只更新差別的部分。如此看來Epoxy真的是MvRx的絕佳助手。關于Epoxy有非常多的內容,查看Epoxy——RecyclerView的絕佳助手了解更多。
Epoxy雖然“高級”,但也僅僅適用于RecyclerView。因此可以看到MvRx的例子中把所有界面的主要部分都以RecyclerView承載,例如,Loading出現在RecyclerView的頭部;如果界面是非滾動的,就把界面作為RecyclerView唯一的元素放入其中,等等。這都是為了使用Epoxy,使開發模式更加統一,并且更加接近于完全的響應式。但是總有些情形下界面不適合用RecyclerView展示,沒關系,我們還可以單獨觀察State中的某(幾)個屬性的改變(這幾乎與LiveData沒有差別)。例如:
//觀察兩個屬性的改變,任意一個屬性方式了改變都會調用
viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
//根據屬性值做更新
}
//觀察Async屬性,可以傳入onSuccess、onFail參數
//和上面觀察普通屬性沒有區別,只是內部幫我們判斷了Async是否成功
viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
})
3. 問題
使用MvRx有幾個問題需要注意:
- State是immutable Kotlin data class,Kotlin幫我們生成了equals方法(即調用每個屬性的equals方法),在ViewModel中通過
setState
,execute
方法更新State時,只有更新后的State確實與上一次的State不相等時,View才會收到通知。經常犯的錯誤是這樣的:
data class CheckedData(
val id: Int,
val name: String,
var checked: Boolean = false
)
//List的equals方法的實現是,項數相同,并且每項都equals
data class SomeState(val data: List<CheckedData> = emptyList()) : MvRxState
class SomeViewModel(initialState: SomeState) : MvRxViewModel<SomeState>(initialState) {
fun setChecked(id: Int) {
setState {
copy(data = data.find { it.id == id }?.checked = true)
}
}
}
這樣做是不行的(也是不允許的),SomeState的data雖然改變了,但對比上一次的SomeState,它們是相等的,因為前后兩個SomeState的data指向了同一塊內存,必然是相等的,因此不會觸發View更新。需要這么做:
fun <T> List<T>.update(newValue: (T) -> T, finder: (T) -> Boolean) = indexOfFirst(finder).let { index ->
if (index >= 0) copy(index, newValue(get(index))) else this
}
fun <T> List<T>.copy(i: Int, value: T): List<T> = toMutableList().apply { set(i, value) }
//最好修改為如下定義,防止直接修改checked屬性
data class CheckedData(
val id: Int,
val name: String,
//只讀的
val checked: Boolean = false
)
class SomeViewModel(initialState: SomeState) : MvRxViewModel<SomeState>(initialState) {
fun setChecked(id: Int) {
setState {
copy(data = data.update({ it.copy(checked = true) }, { it.id == id }))
}
}
}
這樣前后兩個SomeState的data指向不同的內存,并且這兩個data確實不同,會觸發View更新。
緊接著上一點來說,對于State而言,如果改變的值與上次的值相同是不會引起View更新的,這是很合理的行為。但是,如果確實需要在State不變的情況下更新View(例如State中包含的某個屬性更新頻繁,你不想創造太多新對象;或者某些屬性只能在原來的對象上更新,例如SparseArray,查看源碼后發現,壓根兒就不能在State的屬性中使用SparseArray),那么MvRx的確沒有辦法。別忘了,MvRx與Android Architecture Components是并行不悖的,你總是可以使用LiveData去實現。對于MutableLiveData而言,設置相同的值還是會通知其觀察者,是MvRx很好的補充。(但是,并不推薦這么做,因為使用LiveData會破壞State的不可變性,等于你繞開了MvRx,用另外一種方式去傳遞數據,這不利于數據的統一,也不利于數據界面的一致,不到萬不得已不推薦這么做。)
MvRx構建初始的initialState和ViewModel都使用的是反射,并且MvRx支持通過Fragment的arguments構造initialState,然而,大多數時候,ViewModel的initialState是確定的,完全沒有必要通過反射獲取。如果使用MvRx規范中的
fragmentViewModel
等方式獲取,反射是不可避免的,如果追求性能的話,可以通過拷貝fragmentViewModel
的代碼,去除其中的反射,構建自己的獲取ViewModel的方法。雖說MvRx為ViewModel的構建提供了工廠方法,并且這些工廠方法主要目的也是為了依賴注入,但實際上如果真的結合dagger依賴注入的話,你會發現構造ViewModel變得比較麻煩。而且這種做法并沒有利用dagger multiBindings的優勢。實際上dagger可以為ViewModel提供非常友好且便利的
ViewModelProvider.Factory
類(這在Android Architecture Components的sample中已經有展示),但是MvRx卻沒有提供一種方法來使用自定義的ViewModelProvider.Factory
類(見Issues)。在我看來,MvRx最大的特點是響應式,最大的問題也是響應式。因為這種開發模式,與我們之前培養的命令式的開發思維是沖突的,開始的時候總會有種不適應感。最重要的是切換我們的思維方式。
總結
總的來說,MvRx提供了一種Android更純粹響應式開發的可能性。并且以Airbnb的實踐來看,這種可能性已經被擴展到相當廣的范圍。MvRx最適合于那些復雜的RecyclerView界面,通過結合Epoxy,不僅可以大大提高開發效率,而且其提供的響應式思想可以大大簡化我們的思維。其實,有了Epoxy的幫助,絕大部分界面都可以放入RecyclerView中。對于不適宜使用RecyclerView的界面,或者RecyclerView之外的一些界面元素,MvRx至少也提供了與Android Architecture Components相似的能力,并且其與RxJava的結合更加的友好。
MvRx的出現非常符合安迪-比爾定律,硬件的升級遲早會被軟件給消耗掉,或者換種更積極的說法啊,正是因為硬件的發展才給了軟件開發更多的創造力。想想MvRx,由于State是Immutable的,每次更新View必然會產生新的State;想實現真正的響應式,也必然需要浪費更多的計算力,去幫我們計算界面真正更新的部分(實際上我們是可以提前知曉的)。但我覺得這一切都是值得的,畢竟這些許的算力對于現在的手機來說不值一提,但是對于“人”的效率的提升卻是巨大的。還是那句話,最關鍵的因素還是人啊!