Android真响应式架构——MvRx

前言

Android真响应式架构系列文章:

Android真响应式开发——MvRx
Epoxy——RecyclerView的绝佳助手
Android真响应式架构——Model层设计
Android真响应式架构——数据流动性
Android真响应式架构——Epoxy的使用
Android真响应式架构——MvRx和Epoxy的结合

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 = emptyList(),
    val taskRequest: Async> = 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(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(5)
println(foo()) // 5
foo = Fail(IllegalStateException("bar"))
println(foo()) // null

在ViewModel中可以通过扩展函数executeObservable的请求过程包装成Asnyc,这可以方便地表示数据获取的状态(下面会有介绍)。

以上四个核心概念是怎么联系到一起的呢?请看下图:

Android真响应式架构——MvRx_第1张图片
MvRx

图中没有包含AsnycState可包含若干个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。界面张这个样子。

https://upload-images.jianshu.io/upload_images/4803763-cd28075c161e7b28.gif?imageMogr2/auto-orient/strip

以下以首界面为例,介绍应该如何使用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 = emptyList(), //界面上的待办事
    val taskRequest: Async> = 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(initialState) {
    //工厂方法,必须实现MvRxViewModelFactory接口
    companion object : MvRxViewModelFactory {
        /**
         * 主要用途是通过依赖注入传入一些参数来构造ViewModel
         * TasksState是MvRx帮我们构造的(通过反射)
         **/
        override fun create(viewModelContext: ViewModelContext, state: TasksState): BaseMvRxViewModel {
            //例子中并没有使用依赖注入,而是直接获取数据库
            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或者executesetState很好理解,直接更新State即可。其定义如下
abstract class BaseMvRxViewModel {
    //参数是State上的扩展函数,会接收到上次 State的值
    protected fun setState(reducer: S.() -> S) {
        //...
    }
}

因为State是immutable Kotlin data class,所以一般而言都是通过data class的copy方法返回新的State。execute是一个扩展方法,其定义如下

abstract class BaseMvRxViewModel {
    /**
     * Helper to map an observable to an Async property on the state object.
     */
    //参数依然是State上的扩展函数
    fun  Observable.execute(
        stateReducer: S.(Async) -> 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  Observable.execute(
        mapper: (T) -> V,
        successMetaData: ((T) -> Any)? = null,
        stateReducer: S.(Async) -> S
    ): Disposable {
        setState { stateReducer(Loading()) }

        return map {
                val success = Success(mapper(it))
                success.metadata = successMetaData?.invoke(it)
                success as Async
            }
            .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  Observable.execute(
    stateReducer: S.(Async) -> S
)

/**
 * 根据上面execute的定义,我们传递过去的是State上的以Async为参数的扩展函数
 * 因此下面的it参数是指 Async,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类)获取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)
    })

3. 问题

使用MvRx有几个问题需要注意:

  1. 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 = emptyList()) : MvRxState

class SomeViewModel(initialState: SomeState) : MvRxViewModel(initialState) {
    fun setChecked(id: Int) {
        setState {
            copy(data = data.find { it.id == id }?.checked = true)
        }
    }
}

这样做是不行的(也是不允许的),SomeState的data虽然改变了,但对比上一次的SomeState,它们是相等的,因为前后两个SomeState的data指向了同一块内存,必然是相等的,因此不会触发View更新。需要这么做:

fun  List.update(newValue: (T) -> T, finder: (T) -> Boolean) = indexOfFirst(finder).let { index ->
    if (index >= 0) copy(index, newValue(get(index))) else this
}

fun  List.copy(i: Int, value: T): List = toMutableList().apply { set(i, value) }

//最好修改为如下定义,防止直接修改checked属性
data class CheckedData(
    val id: Int,
    val name: String,
    //只读的
    val checked: Boolean = false
)

class SomeViewModel(initialState: SomeState) : MvRxViewModel(initialState) {
    fun setChecked(id: Int) {
        setState {
            copy(data = data.update({ it.copy(checked = true) }, { it.id == id }))
        }
    }
}

这样前后两个SomeState的data指向不同的内存,并且这两个data确实不同,会触发View更新。

  1. 紧接着上一点来说,对于State而言,如果改变的值与上次的值相同是不会引起View更新的,这是很合理的行为。但是,如果确实需要在State不变的情况下更新View(例如State中包含的某个属性更新频繁,你不想创造太多新对象;或者某些属性只能在原来的对象上更新,例如SparseArray,查看源码后发现,压根儿就不能在State的属性中使用SparseArray),那么MvRx的确没有办法。别忘了,MvRx与Android Architecture Components是并行不悖的,你总是可以使用LiveData去实现。对于MutableLiveData而言,设置相同的值还是会通知其观察者,是MvRx很好的补充。(但是,并不推荐这么做,因为使用LiveData会破坏State的不可变性,等于你绕开了MvRx,用另外一种方式去传递数据,这不利于数据的统一,也不利于数据界面的一致,不到万不得已不推荐这么做。)

  2. MvRx构建初始的initialState和ViewModel都使用的是反射,并且MvRx支持通过Fragment的arguments构造initialState,然而,大多数时候,ViewModel的initialState是确定的,完全没有必要通过反射获取。如果使用MvRx规范中的fragmentViewModel等方式获取,反射是不可避免的,如果追求性能的话,可以通过拷贝fragmentViewModel的代码,去除其中的反射,构建自己的获取ViewModel的方法。

  3. 虽说MvRx为ViewModel的构建提供了工厂方法,并且这些工厂方法主要目的也是为了依赖注入,但实际上如果真的结合dagger依赖注入的话,你会发现构造ViewModel变得比较麻烦。而且这种做法并没有利用dagger multiBindings的优势。实际上dagger可以为ViewModel提供非常友好且便利的ViewModelProvider.Factory类(这在Android Architecture Components的sample中已经有展示),但是MvRx却没有提供一种方法来使用自定义的ViewModelProvider.Factory类(见Issues)。

  4. 在我看来,MvRx最大的特点是响应式,最大的问题也是响应式。因为这种开发模式,与我们之前培养的命令式的开发思维是冲突的,开始的时候总会有种不适应感。最重要的是切换我们的思维方式。

总结

总的来说,MvRx提供了一种Android更纯粹响应式开发的可能性。并且以Airbnb的实践来看,这种可能性已经被扩展到相当广的范围。MvRx最适合于那些复杂的RecyclerView界面,通过结合Epoxy,不仅可以大大提高开发效率,而且其提供的响应式思想可以大大简化我们的思维。其实,有了Epoxy的帮助,绝大部分界面都可以放入RecyclerView中。对于不适宜使用RecyclerView的界面,或者RecyclerView之外的一些界面元素,MvRx至少也提供了与Android Architecture Components相似的能力,并且其与RxJava的结合更加的友好。
MvRx的出现非常符合安迪-比尔定律,硬件的升级迟早会被软件给消耗掉,或者换种更积极的说法啊,正是因为硬件的发展才给了软件开发更多的创造力。想想MvRx,由于State是Immutable的,每次更新View必然会产生新的State;想实现真正的响应式,也必然需要浪费更多的计算力,去帮我们计算界面真正更新的部分(实际上我们是可以提前知晓的)。但我觉得这一切都是值得的,毕竟这些许的算力对于现在的手机来说不值一提,但是对于“人”的效率的提升却是巨大的。还是那句话,最关键的因素还是人啊!

你可能感兴趣的:(Android真响应式架构——MvRx)