先来认识一下 MvRx 框架

3,287 阅读4分钟

MvRx Android on Autopilot 响应式框架

简介

Mavericks (MvRx) 是 Airbnb 开源的一个基于 MVI 的 Android 框架,可以帮助我们更快、更容易的构建项目,构建在 Android JetpackKotlin Coroutines 之上,因此可以将其视为对 Google 标准库的补充。

为什么使用 MvRx?

MvRx 提供:

  • 状态管理: 应用程序的状态是一种数据结构,可以在任何给定时间表示应用程序的属性。例如,在社交媒体应用程序中,状态可能包含用户信息和最近发布的列表。状态管理是指在各种条件下保持状态并从一种状态变为另一种状态的任务。 MvRx 使状态管理变得简单灵活。

  • 与 Android 架构组件集成: MvRx 构建在 Android 架构组件(如 ViewModelLifecycle )之上。使得在已经在使用架构组件的项目中逐步采用变得容易。

  • 概念上基于 React: React 是一种流行的 Web 框架,可帮助构建响应式应用程序。MvRxReact 的概念引入 Android,因此可以利用其功能。

那么 MvRx 是如何工作的呢?接下来了解它的核心概念。

核心概念

掌握 MvRx 需要使用三个类:MavericksStateMavericksViewModelMavericksView

MavericksState

MavericksState 是一个不可变的 Kotlin 类 immutable data class,包含表示屏幕所需的所有属性。需要是不可变的,以便可以从不同的线程安全地访问,修改状态的唯一方法是在 copy() 上使用运算符。每个状态类都应该实现 MavericksState

data class UserState(
    val score: Int = 0,
    val previousHighScore: Int = 150,
    val livesLeft: Int = 99
) : MavericksState

因为 state 只是一个普通的数据类,可以创建属性来表示特定的状态。

data class UserState(
    val score: Int = 0,
    val previousHighScore: Int = 150,
    val livesLeft: Int = 99,
) : MavericksState {
    // Properties inside the body of your state class are "derived".
    val pointsUntilHighScore = (previousHighScore - score).coerceAtLeast(0)
    val isHighScore = score >= previousHighScore
}

MavericksViewModel

ViewModel 的扩展类,主要区别在于 MavericksViewModel 它依赖于单个不可变 MavericksState 实例。不依赖 LiveData 来通知更改。MavericksViewModel 包含可以修改状态的方法, View 只能使用这些函数来修改状态。

setState { copy(yourProp = newValue) }

订阅状态更改

// Invoked every time state changes
onEach { state ->
}
// Invoked whenever propA changes only.
onEach(YourState::propA) { a ->
}
// Invoked whenever propA, propB, or propC changes only.
onEach(YourState::propA, YourState::propB, YourState::propC) { a, b, c ->
}

MavericksView

用户查看视图的 UI 并与之交互,该视图可以是 FragmentActivity。需要观察状态变化的视图必须扩展 MavericksView 接口。

MavericksView 有一个 invalidate() 视图必须实现的方法。每当状态的任何属性发生变化时,它都会调用 invalidate() 并更新视图。

视图可以使用 MavericksView 如下方式访问状态:

withState(viewModel) { state ->
  ...
}

异步

Mavericks 使处理异步请求(例如从网络、数据库或其他任何异步获取)变得容易。Async<T> 是一个有四个子类的 密封类

sealed class Async<out T>(private val value: T?) {

    open operator fun invoke(): T? = value

    object Uninitialized : Async<Nothing>(value = null)

    data class Loading<out T>(private val value: T? = null) : Async<T>(value = value)

    data class Success<out T>(private val value: T) : Async<T>(value = value) {
        override operator fun invoke(): T = value
    }

    data class Fail<out T>(val error: Throwable, private val value: T? = null) : Async<T>(value = value)
}
  • Uninitialized: 表示还没有值。
  • Loading: 表示该字段的值正在加载。
  • Success: 标识数据加载成功。此类型有一个名为 value 的属性,其中包含您需要的实际数据。
  • Fail: 表示请求发生错误,此类型有一个名为 error 的属性,该属性提供带有错误消息的异常。

订阅异步属性

如果 state 属性是 Async,可以使用 onAsync 而不是 onEach 订阅状态更改。

data class MyState(val name: Async<String>) : MavericksState
...
onAsync(MyState::name) { name ->
    // Called when name is Success and any time it changes.
}

// Or if you want to handle failures
onAsync(
    MyState::name,
    onFail = { e -> .... },
    onSuccess = { name -> ... }
)

一个 MvRx 结构的简单示例

/** State classes contain all of the data you need to render a screen. */
data class CounterState(val count: Int = 0) : MavericksState

/** ViewModels are where all of your business logic lives. It has a simple lifecycle and is easy to test. */
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
    fun incrementCount() = setState { copy(count = count + 1) }
}

/**
 * Fragments in Mavericks are simple and rarely do more than bind your state to views.
 * Mavericks works well with Fragments but you can use it with whatever view architecture you use.
 */
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {
    private val viewModel: CounterViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        counterText.setOnClickListener {
            viewModel.incrementCount()
        }
    }

    override fun invalidate() = withState(viewModel) { state ->
        counterText.text = "Count: ${state.count}"
    }
}