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()方法,從而通知界面重繪。

完全繼承自 Architecture Components中的 ViewModel,ViewModel 包含有除了界面顯示之外的業務邏輯。此外,最關鍵的一點是,ViewModel 還包含有一個State,ViewModel 可以改變 State 的狀態,然后 View 可以觀察 State 的狀態。實現類需繼承 BaseMvRxViewModel,并且必須向 BaseMvRxViewModel 傳遞 initialState(代表了View 的初始狀態)。像是這樣

class TasksViewModel(initialState: TasksState) : BaseMvRxViewModel<TasksState>(initialState)

一般而言是一個繼承自 BaseMvRxFragment 的 Fragment。BaseMvRxFragment 實現了接口 MvRxView,這個接口有一個 invalidate() 方法,每當 ViewModel 的 state 發生改變時 invalidate() 方法都會被調用。View 也可以觀察 State 中的某個或某幾個屬性的變化,View 是沒辦法改變 State 狀態的,只有 ViewModel 可以改變 State 的狀態。

代表了數據加載的狀態。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 應該怎樣使用。

如何使用

ToDo Sample,架構界的 Hello World。界面長這個樣子。

以下以首界面為例,介紹應該如何使用 MvRx。

//待辦事的定義,包含有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 包含了這個界面要顯示的所有數據

具體的業務邏輯并不重要,主要看 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。這里有幾點需要說明:

  1. 按照 MvRx 的要求,ViewModel 可以沒有工廠方法,這樣的話 MvRx 會通過反射構造出 ViewModel(當然這一般不可能,畢竟 ViewModel 一般都包含 Model 層)。如果 ViewModel 包含有除 initialState 之外的其它構造參數,則需要我們實現工廠方法。如上所示,必須通過伴生對象實現 MvRxViewModelFactory 接口。
  2. 只能在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) }
}

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 會幫我們完成如下幾件事:

  1. activityViewModel, fragmentViewModel, existingViewModel其實都是 Kotlin 的Lazy 子類,顯然會是懶加載。但是它不是真正的“懶”,因為在這些子類的構造函數中會添加一個對 View 生命周期的觀察者,在 ON_CREATE 事件發生時會構造出ViewModel,也就是說 ViewModel 最晚到 ON_CREATE 時即被構造完成(為了及早發出網絡請求等)。
  2. 通過反射構造出 State,ViewModel。
  3. 調用 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)
})

問題

使用 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;想實現真正的響應式,也必然需要浪費更多的計算力,去幫我們計算界面真正更新的部分(實際上我們是可以提前知曉的)。但我覺得這一切都是值得的,畢竟這些許的算力對于現在的手機來說不值一提,但是對于“人”的效率的提升卻是巨大的。還是那句話,最關鍵的因素還是人??!

本文章轉載微信公眾號@郭霖

上一篇:

【AIGC】 一文帶你了解什么是AIGC!(全面詳解)

下一篇:

數據庫融入DevOps基因后,運維再也不用做背鍋俠了!
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費