val tasks: List<Task> = emptyList(),
val taskRequest: Async<List<Task>> = Uninitialized,
val isLoading: Boolean = false,
val lastEditedTask: String? = null
) : MvRxState //MvRxState 僅是一個(gè)標(biāo)記接口

State 的作用是承載數(shù)據(jù),并且應(yīng)該包含有界面顯示的所有數(shù)據(jù)。當(dāng)然可以對界面進(jìn)行拆分,使用多個(gè)State共同決定界面的顯示。

State必須是不可變的(immutable),即State的所有屬性必須是val的。只有ViewModel 可以改變 State,改變 State 時(shí)一般使用其 copy 方法,創(chuàng)建一個(gè)新的 State對象。

可以把 MvRx 的 State 類比成 Architecture Components 中的 LiveData,它們的相同點(diǎn)是都可以被 View 觀察,不同點(diǎn)是,State 的改變會觸發(fā) View 的 invalidate()方法,從而通知界面重繪。

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

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

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

代表了數(shù)據(jù)加載的狀態(tài)。Async 是一個(gè)Kotlin sealed class,它有四種類型:Uninitialized, Loading, Success, Fail(包含了一個(gè)名為 error 的屬性,可以獲取錯(cuò)誤類型)。Async 重載了操作符 invoke,除了在 Success 返回數(shù)據(jù)外,其它情況下都返回null:

var foo = Loading()
println(foo()) // null
foo = Success<Int>(5)
println(foo()) // 5
foo = Fail(IllegalStateException("bar"))
println(foo()) // null

在 ViewModel 中可以通過擴(kuò)展函數(shù)execute把Observable<T>的請求過程包裝成Asnyc<T>,這可以方便地表示數(shù)據(jù)獲取的狀態(tài)(下面會有介紹)。

以上四個(gè)核心概念是怎么聯(lián)系到一起的呢?請看下圖:

圖中沒有包含 Asnyc,State 可包含若干個(gè) Asnyc,用來表示數(shù)據(jù)加載的狀態(tài),便于顯示Loading 或者加載錯(cuò)誤信息等。

按照理想情形,View 不需要主動觀察 State,State 的任意改變都會調(diào)用 View 的invalidate方法,在 invalidate 方法中根據(jù)當(dāng)前的 State(在 View 中通過 ViewModel 的withState 方法獲取 State)直接重繪一下 View 即可。然而這太過于理想,實(shí)際上可以通過 selectSubscribe,asyncSubscribe 等方法觀察 State 中某個(gè)屬性的改變,根據(jù)特定的屬性更新 View 的特定部分。

以上是 MvRx 的四個(gè)核心概念。下面以官方 sample 為例,展示一下 MvRx 應(yīng)該怎樣使用。

如何使用

ToDo Sample,架構(gòu)界的 Hello World。界面長這個(gè)樣子。

以下以首界面為例,介紹應(yīng)該如何使用 MvRx。

//待辦事的定義,包含有id, title, description以及是否完成標(biāo)志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, //代表請求的狀態(tài)
val isLoading: Boolean = false, //是否顯示Loading
val lastEditedTask: String? = null //上次編輯的待辦事ID
) : MvRxState

State 包含了這個(gè)界面要顯示的所有數(shù)據(jù)

具體的業(yè)務(wù)邏輯并不重要,主要看 ViewModel 是如何定義的。

/**
* 必須有一個(gè)initialState
* source是數(shù)據(jù)源,可以是數(shù)據(jù)庫,也可以是網(wǎng)絡(luò)請求等(例子中是數(shù)據(jù)庫)
**/
class TasksViewModel(initialState: TasksState, private val source: TasksDataSource) : MvRxViewModel<TasksState>(initialState) {
//工廠方法,必須實(shí)現(xiàn)MvRxViewModelFactory接口
companion object : MvRxViewModelFactory<TasksViewModel, TasksState> {
/**
* 主要用途是通過依賴注入傳入一些參數(shù)來構(gòu)造ViewModel
* TasksState是MvRx幫我們構(gòu)造的(通過反射)
**/
override fun create(viewModelContext: ViewModelContext, state: TasksState): BaseMvRxViewModel<TasksState> {
//例子中并沒有使用依賴注入,而是直接獲取數(shù)據(jù)庫
val database = ToDoDatabase.getInstance(viewModelContext.activity)
val dataSource = DatabaseDataSource(database.taskDao(), 2000)
return TasksViewModel(state, dataSource)
}
}

init {
//方便調(diào)試,State狀態(tài)改變時(shí)打印出來
logStateChanges()
//初始加載任務(wù)
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的狀態(tài)
setState { copy(tasks = tasks.upsert(task) { it.id == task.id }, lastEditedTask = task.id) }
//因?yàn)槭菙?shù)據(jù)庫操作,一般不會失敗,所以沒有理會數(shù)據(jù)操作的狀態(tài)
source.upsertTask(task)
}

//標(biāo)記任務(wù)完成與否
fun setComplete(id: String, complete: Boolean) {
setState {
//沒有這個(gè)任務(wù),拉倒;this指之前的 State,直接返回之前的 State意思就是無需更新
val task = tasks.findTask(id) ?: return@setState this
//這個(gè)任務(wù)已經(jīng)完成了,拉倒
if (task.complete == complete) return@setState this
//找到這個(gè)任務(wù),并更新
copy(tasks = tasks.copy(tasks.indexOf(task), task.copy(complete = complete)), lastEditedTask = id)
}
//數(shù)據(jù)庫更新
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 實(shí)現(xiàn)了業(yè)務(wù)邏輯,其核心作用就是與 Model 層(這里的 source)溝通,并更新 State。這里有幾點(diǎn)需要說明:

  1. 按照 MvRx 的要求,ViewModel 可以沒有工廠方法,這樣的話 MvRx 會通過反射構(gòu)造出 ViewModel(當(dāng)然這一般不可能,畢竟 ViewModel 一般都包含 Model 層)。如果 ViewModel 包含有除 initialState 之外的其它構(gòu)造參數(shù),則需要我們實(shí)現(xiàn)工廠方法。如上所示,必須通過伴生對象實(shí)現(xiàn) MvRxViewModelFactory 接口。
  2. 只能在ViewModel中更新State。更新State有兩種方法,setState或者 execute。setState 很好理解,直接更新 State 即可。其定義如下
abstract class BaseMvRxViewModel<S : MvRxState> {
//參數(shù)是State上的擴(kuò)展函數(shù),會接收到上次 State的值
protected fun setState(reducer: S.() -> S) {
//...
}
}

因?yàn)?State 是 immutable Kotlin data class,所以一般而言都是通過 data class 的 copy方法返回新的 State。execute 是一個(gè)擴(kuò)展方法,其定義如下

abstract class BaseMvRxViewModel<S : MvRxState> {
/**
* Helper to map an observable to an Async property on the state object.
*/
//參數(shù)依然是State上的擴(kuò)展函數(shù)
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的時(shí)候dispose } }

execute 方法可以把 Observable 的請求過程包裝成 Async,我們都知道訂閱 Observable 需要有 onNext,onComplete,onError 等方法,execute 就是把這些個(gè)方法包裝成了統(tǒng)一的 Async 類。前面已經(jīng)說過,Async是sealed class,只有四個(gè)子類:Uninitialized, Loading, Success, Fail。這些子類完美的描述了一次請求的過程,并且它們重載了 invoke 操作符(Success 情況下返回請求的數(shù)據(jù),其它情況均為 null)。因此經(jīng)常看到這樣的樣板代碼:

fun <T> Observable<T>.execute(
stateReducer: S.(Async<T>) -> S
)

/**
* 根據(jù)上面execute的定義,我們傳遞過去的是State上的以Async<T>為參數(shù)的擴(kuò)展函數(shù)
* 因此下面的it參數(shù)是指 Async<T>,it()是獲取請求的結(jié)果,tasks = it() ?: tasks 表示只在請求 Success時(shí)更新State
**/
fun refreshTasks() {
source.getTasks()
//...
.execute { copy(taskRequest = it, tasks = it() ?: tasks, lastEditedTask = null) }
}

View 的使用

abstract class BaseFragment : BaseMvRxFragment() {
//activityViewModel是MvRx定義的獲取ViewModel的方式
//按照規(guī)范必須使用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中某個(gè)(某幾個(gè))屬性的變化
viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
//...
}

//觀察Async屬性
viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
})
}

//State的改變均會觸發(fā)
override fun invalidate() {
//Epoxy的用法
recyclerView.requestModelBuild()
}

abstract fun epoxyController(): ToDoEpoxyController
}

class TaskListFragment : BaseFragment() {
//另一個(gè)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的規(guī)范,View通過activityViewModel(ViewModel被置于Activity中), fragmentViewModel(ViewModel被置于 Fragment 中), existingViewModel(從Activity中獲取已存在的 ViewModel) 來獲取ViewModel,這是因?yàn)椋赃@幾種方式獲取ViewModel,MvRx 會幫我們完成如下幾件事:

  1. activityViewModel, fragmentViewModel, existingViewModel其實(shí)都是 Kotlin 的Lazy 子類,顯然會是懶加載。但是它不是真正的“懶”,因?yàn)樵谶@些子類的構(gòu)造函數(shù)中會添加一個(gè)對 View 生命周期的觀察者,在 ON_CREATE 事件發(fā)生時(shí)會構(gòu)造出ViewModel,也就是說 ViewModel 最晚到 ON_CREATE 時(shí)即被構(gòu)造完成(為了及早發(fā)出網(wǎng)絡(luò)請求等)。
  2. 通過反射構(gòu)造出 State,ViewModel。
  3. 調(diào)用 ViewModel 的 subscribe 方法,觀察 State 的改變,如果改變則調(diào)用 View 的invalidate 方法。

當(dāng) State 發(fā)生改變時(shí),View 的 invalidate 方法會被調(diào)用。invalidate被調(diào)用僅說明了State 發(fā)生了改變,究竟是哪個(gè)屬性發(fā)生的改變并不得而知,按照 MvRx 的“理想”,哪個(gè)屬性發(fā)生改變并不重要,只要 View 根據(jù)當(dāng)前的 State“重繪”一下 View 即可。這里“重繪”顯然指的不是簡單地重繪整個(gè)界面,應(yīng)該是根據(jù)當(dāng)前 State“描繪”當(dāng)前界面,然后與上次界面作比較,只更新差異部分。顯然這種“理想”太過于高級,需要有一個(gè)幫手來完成這項(xiàng)任務(wù),于是就有了 Epoxy(其實(shí)是先有的 Epoxy)。

Epoxy 簡單來說就是 RecyclerView的高級助手,我們只需要定義某個(gè)數(shù)據(jù)在RecyclerView 的 ItemView 上是如何顯示的,然后把一堆數(shù)據(jù)扔給 Epoxy 就行了。Epoxy會幫我們分析這次的數(shù)據(jù)跟上次的數(shù)據(jù)有什么差別,只更新差別的部分。如此看來Epoxy真的是MvRx的絕佳助手。關(guān)于Epoxy有非常多的內(nèi)容,查看Epoxy——RecyclerView 的絕佳助手了解更多。

Epoxy 雖然“高級”,但也僅僅適用于 RecyclerView。因此可以看到 MvRx 的例子中把所有界面的主要部分都以 RecyclerView 承載,例如,Loading 出現(xiàn)在 RecyclerView 的頭部;如果界面是非滾動的,就把界面作為RecyclerView唯一的元素放入其中,等等。這都是為了使用 Epoxy,使開發(fā)模式更加統(tǒng)一,并且更加接近于完全的響應(yīng)式。但是總有些情形下界面不適合用 RecyclerView 展示,沒關(guān)系,我們還可以單獨(dú)觀察 State 中的某(幾)個(gè)屬性的改變(這幾乎與 LiveData 沒有差別)。例如:

//觀察兩個(gè)屬性的改變,任意一個(gè)屬性方式了改變都會調(diào)用
viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
//根據(jù)屬性值做更新
}

//觀察Async屬性,可以傳入onSuccess、onFail參數(shù)
//和上面觀察普通屬性沒有區(qū)別,只是內(nèi)部幫我們判斷了Async是否成功
viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
})

問題

使用 MvRx 有幾個(gè)問題需要注意

State 是 immutable Kotlin data class,Kotlin 幫我們生成了equals方法(即調(diào)用每個(gè)屬性的 equals 方法),在 ViewModel 中通過 setState,execute 方法更新State時(shí),只有更新后的 State 確實(shí)與上一次的 State 不相等時(shí),View 才會收到通知。經(jīng)常犯的錯(cuò)誤是這樣的:

data class CheckedData(
val id: Int,
val name: String,
var checked: Boolean = false
)

//List的equals方法的實(shí)現(xiàn)是,項(xiàng)數(shù)相同,并且每項(xiàng)都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,它們是相等的,因?yàn)榍昂髢蓚€(gè) SomeState 的 data 指向了同一塊內(nèi)存,必然是相等的,因此不會觸發(fā) 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 }))
}
}
}

這樣前后兩個(gè) SomeState 的 data 指向不同的內(nèi)存,并且這兩個(gè) data 確實(shí)不同,會觸發(fā)View 更新。

緊接著上一點(diǎn)來說,對于 State 而言,如果改變的值與上次的值相同是不會引起 View更新的,這是很合理的行為。但是,如果確實(shí)需要在State不變的情況下更新View(例如 State 中包含的某個(gè)屬性更新頻繁,你不想創(chuàng)造太多新對象;或者某些屬性只能在原來的對象上更新,例如 SparseArray,查看源碼后發(fā)現(xiàn),壓根兒就不能在State 的屬性中使用 SparseArray),那么 MvRx 的確沒有辦法。別忘了,MvRx 與Android Architecture Components 是并行不悖的,你總是可以使用 LiveData 去實(shí)現(xiàn)。對于 MutableLiveData 而言,設(shè)置相同的值還是會通知其觀察者,是MvRx 很好的補(bǔ)充。(但是,并不推薦這么做,因?yàn)槭褂?LiveData 會破壞 State 的不可變性,等于你繞開了 MvRx,用另外一種方式去傳遞數(shù)據(jù),這不利于數(shù)據(jù)的統(tǒng)一,也不利于數(shù)據(jù)界面的一致,不到萬不得已不推薦這么做。)

MvRx 構(gòu)建初始的 initialState 和 ViewModel 都使用的是反射,并且 MvRx 支持通過 Fragment 的 arguments 構(gòu)造 initialState,然而,大多數(shù)時(shí)候,ViewModel 的initialState是確定的,完全沒有必要通過反射獲取。如果使用 MvRx 規(guī)范中的fragmentViewModel 等方式獲取,反射是不可避免的,如果追求性能的話,可以通過拷貝fragmentViewModel的代碼,去除其中的反射,構(gòu)建自己的獲取ViewModel的方法。

雖說 MvRx 為 ViewModel 的構(gòu)建提供了工廠方法,并且這些工廠方法主要目的也是為了依賴注入,但實(shí)際上如果真的結(jié)合dagger依賴注入的話,你會發(fā)現(xiàn)構(gòu)造ViewModel 變得比較麻煩。而且這種做法并沒有利用 dagger multiBindings 的優(yōu)勢。實(shí)際上dagger可以為ViewModel提供非常友好且便利的ViewModelProvider.Factory類(這在Android Architecture Components的sample中已經(jīng)有展示),但是MvRx卻沒有提供一種方法來使用自定義的ViewModelProvider.Factory類(見Issues)。

在我看來,MvRx 最大的特點(diǎn)是響應(yīng)式,最大的問題也是響應(yīng)式。因?yàn)檫@種開發(fā)模式,與我們之前培養(yǎng)的命令式的開發(fā)思維是沖突的,開始的時(shí)候總會有種不適應(yīng)感。最重要的是切換我們的思維方式。

總結(jié)

總的來說,MvRx 提供了一種 Android 更純粹響應(yīng)式開發(fā)的可能性。并且以 Airbnb 的實(shí)踐來看,這種可能性已經(jīng)被擴(kuò)展到相當(dāng)廣的范圍。MvRx 最適合于那些復(fù)雜的RecyclerView 界面,通過結(jié)合 Epoxy,不僅可以大大提高開發(fā)效率,而且其提供的響應(yīng)式思想可以大大簡化我們的思維。其實(shí),有了 Epoxy 的幫助,絕大部分界面都可以放入RecyclerView 中。對于不適宜使用 RecyclerView 的界面,或者 RecyclerView 之外的一些界面元素,MvRx 至少也提供了與 Android Architecture Components 相似的能力,并且其與 RxJava 的結(jié)合更加的友好。

MvRx 的出現(xiàn)非常符合安迪-比爾定律,硬件的升級遲早會被軟件給消耗掉,或者換種更積極的說法啊,正是因?yàn)橛布陌l(fā)展才給了軟件開發(fā)更多的創(chuàng)造力。想想 MvRx,由于 State是 Immutable 的,每次更新 View 必然會產(chǎn)生新的 State;想實(shí)現(xiàn)真正的響應(yīng)式,也必然需要浪費(fèi)更多的計(jì)算力,去幫我們計(jì)算界面真正更新的部分(實(shí)際上我們是可以提前知曉的)。但我覺得這一切都是值得的,畢竟這些許的算力對于現(xiàn)在的手機(jī)來說不值一提,但是對于“人”的效率的提升卻是巨大的。還是那句話,最關(guān)鍵的因素還是人啊!

本文章轉(zhuǎn)載微信公眾號@郭霖

上一篇:

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

下一篇:

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

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

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

10個(gè)渠道
一鍵對比試用API 限時(shí)免費(fèi)