前言
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;想实现真正的响应式,也必然需要浪费更多的计算力,去帮我们计算界面真正更新的部分(实际上我们是可以提前知晓的)。但我觉得这一切都是值得的,毕竟这些许的算力对于现在的手机来说不值一提,但是对于“人”的效率的提升却是巨大的。还是那句话,最关键的因素还是人啊!