Mavericks 是 Airbnb 的 Android MVI 框架,是一种响应式的 UI 框架。
Mavericks 建立在以下现有技术和概念之上:
Kotlin 语言
Android Architecture Components(Lifecycle、ViewModel 等)
Kotlin Flow
React (概念)
Mavericks 吸收了来自 React 的响应式思想,同时 Mavericks 依赖了 Kotlin、AAC(Android Architecture Components)、Flow 等技术,实现了一个 Android 原生的响应式 UI 框架。
指路->:https://github.com/airbnb/mavericks
This is what it looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 data class HelloWorldState (val title: String = "Hello World" ) : MavericksStateclass HelloWorldViewModel (initialState: HelloWorldState) : MavericksViewModel<HelloWorldState>(initialState) { fun getMoreExcited () = setState { copy(title = "$title !" ) } }class HelloWorldFragment : Fragment (R.layout.hello_world_fragment), MavericksView { private val viewModel: HelloWorldViewModel by fragmentViewModel() override fun invalidate () = withState(viewModel) { state -> } }
依赖:
1 2 3 dependencies { implementation 'com.airbnb.android:mavericks:x.y.z' }
官方文档网站:Mavericks Docs (airbnb.io)
Mavericks 建立在 Android Jetpack 和 Kotlin 协程 之上,因此可以将其视为对 Google 标准库集的补充,而不是背离。
Mavericks 的 1.0 版本 MVRX 是建立在 Rxjava 的基础之上,但是,在如今大部分 APP 都切换到 kotlin 协程之后,很多 App 都不再有 RxJava 的依赖了,因此,AirBnb 使用 Kotlin Flow 重写了该框架,也就是 Mavericks 2.0。
为什么我们需要它?
减少 50%到 75%代码,更简洁高效的开发
真正的响应式,数据与 UI 解耦,更清晰的代码
与 Kotlin、AAC 的结合与包容,更符合技术趋势
React 思想在安卓的最佳实践
单向数据流、响应式 UI 架构模式:
MVI(Model-View-Intent) 和 React 是两种常用的 UI 架构模式,分别用于 Android 和前端开发。MVI 和 React 都是现代 UI 架构模式,强调单向数据流和不可变状态。尽管它们应用于不同的平台,但它们在设计理念上有许多相似之处,特别是在单向数据流的概念上。
MVI(Model-View-Intent) MVI 是一种用于 Android 应用的架构模式,它强调单向数据流和不可变状态。MVI 由三个主要部分组成:
Model : 表示应用的状态。Model 通常是不可变的,这意味着每次状态变化都会创建一个新的 Model 实例。
View : 负责呈现 UI 并接收用户输入。View 将用户输入转换为 Intent。
Intent : 表示用户的意图或动作。Intent 被发送到 Model,Model 根据 Intent 更新状态。
数据流
用户交互 : 用户与 View 交互,触发一个 Intent。
Intent 处理 : Intent 被发送到 Model,Model 处理 Intent 并更新状态。
状态更新 : 更新后的状态通过单向数据流传递回 View,更新 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 data class ViewState (val count: Int )sealed class ViewIntent { object Increment : ViewIntent() object Decrement : ViewIntent() }class MainViewModel : ViewModel () { private val _viewState = MutableStateFlow(ViewState(0 )) val viewState: StateFlow<ViewState> = _viewState fun processIntent (intent: ViewIntent ) { when (intent) { is ViewIntent.Increment -> _viewState.value = _viewState.value.copy(count = _viewState.value.count + 1 ) is ViewIntent.Decrement -> _viewState.value = _viewState.value.copy(count = _viewState.value.count - 1 ) } } }@Composable fun MainScreen (viewModel: MainViewModel ) { val viewState by viewModel.viewState.collectAsState() Column { Text("Count: ${viewState.count} " ) Button(onClick = { viewModel.processIntent(ViewIntent.Increment) }) { Text("Increment" ) } Button(onClick = { viewModel.processIntent(ViewIntent.Decrement) }) { Text("Decrement" ) } } }
React + Redux React 是一个用于构建用户界面的 JavaScript 库。React 也强调单向数据流和不可变状态。React 的核心概念包括:
Component : React 应用由组件组成。每个组件描述了 UI 的一部分。
State : 组件可以拥有状态。状态变化会触发组件重新渲染。
Props : 组件可以接收外部传入的数据,这些数据称为 Props。
Redux 是一个用于 JavaScript 应用的状态管理库,通常与 React 一起使用,但也可以与其他 JavaScript 框架或库一起使用。Redux 提供了一种可预测的方式来管理应用的状态,使得状态变化更加透明和可调试。
Redux 的设计基于几个核心概念:
单一数据源(Single Source of Truth) :
应用的整个状态被存储在一个对象树中,这个对象树只存在于一个单一的 store 中。
状态是只读的(State is Read-Only) :
唯一改变状态的方法是触发一个 action。这样可以确保状态的变化是可预测的和可追踪的。
使用纯函数来执行修改(Changes are Made with Pure Functions) :
为了描述状态树如何通过 actions 转变成下一个状态,你需要编写纯函数(reducers)。
数据流
用户交互 : 用户与组件交互,触发状态变化。
状态更新 : 状态变化会触发组件重新渲染。
单向数据流 : 数据从父组件流向子组件,子组件接收事件更改状态,状态改变触发 UI 更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React from "react" ;import ReactDOM from "react-dom" ;import { Provider , useDispatch, useSelector } from "react-redux" ;import { createStore } from "redux" ;function counter (state = 0 , action ) { switch (action.type ) { case "INCREMENT" : return state + 1 ; case "DECREMENT" : return state - 1 ; default : return state; } }const store = createStore (counter);function Counter ( ) { const count = useSelector ((state ) => state); const dispatch = useDispatch (); return ( <div > <p > Count: {count}</p > <button onClick ={() => dispatch({ type: "INCREMENT" })}>Increment</button > <button onClick ={() => dispatch({ type: "DECREMENT" })}>Decrement</button > </div > ); }ReactDOM .render ( <Provider store ={store} > <Counter /> </Provider > , document .getElementById ("root" ) );
MVI 和 React + Redux 两者具有相同的响应式思想和架构模式:
不可变状态 :
MVI : 状态是不可变的,每次状态变化都会创建一个新的状态对象。
React + Redux : Redux 强调使用不可变状态,每次状态变化都会创建一个新的状态对象。
单向数据流 :
MVI : 数据流是单向的,从 Intent 到 Model,再从 Model 到 View。
React + Redux : 数据流也是单向的,从 Action 到 Reducer,再从 Store 到 View。
纯函数 :
MVI : Model 更新是通过纯函数处理的,即 Reducer。
React + Redux : Redux 中的 Reducer 是纯函数,负责根据 Action 更新状态。
Airbnb Mavericks 框架 而对于 Airbnb 的 Mavericks 来说,也是借鉴和继承了相同的响应式思想,下面我们可以来对比一下:
状态不可变:
MavericksState 的状态使用 data class 定义,是不可变的,要修改状态,必须用data class 的clone方法。每次状态变化,都需要重新创建一个新的对象。
1 2 3 4 5 data class UserState ( val score : Int = 0 , val previousHighScore : Int = 150 , val livesLeft : Int = 99 , ) : MavericksState
单向数据流:
通过 kotlin flow 实现 数据 (state)的单向流动。
状态更新:Mavericks 通过 setState 方法,异步更新 state,由于 state 为 data class ,更新状态时直接使用 copy 方法获取新对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 protected fun setState (reducer: S.() -> S ) { } setState { copy (yourProp = newValue) } private suspend fun flushQueuesOnce ( ) { select<Unit > { setStateChannel.onReceive { reducer -> val newState = state.reducer () if (newState != state) { state = newState stateSharedFlow.emit (newState) } } withStateChannel.onReceive { block -> block (state) } } }
状态监听:最新的状态 通过 stateSharedFlow 发送,通过 onEach 监听 收集 flow 中的最新状态。
1 2 3 4 5 6 onEach (YourState::propA ) { a -> }onEach (YourState::propA, YourState::propB, YourState::propC ) { a, b, c -> }
纯函数:
Mavericks 中的 setState { } 代码块函数,也就是 reducer: S.() -> S 这部分是纯函数。
纯函数(Pure Function)是函数式编程中的一个重要概念。一个函数要被认为是纯函数,需要满足以下两个条件:
确定性 :对于相同的输入,函数总是返回相同的输出。
无副作用 :函数的执行不会影响外部状态,也不会依赖于外部可变状态。
在 Mavericks,会对 reducer 函数执行两次, 进行纯函数检查:
1 2 3 4 val firstState = this .reducer () val secondState = this .reducer ()if (firstState != secondState) { }
例如,如下写法 reducer 就不是纯函数,对于相同的输入,两次执行结果不一致。
1 setState { copy (time = System .currentTimeMillis ()) }
怎么使用? 添加依赖:
1 2 3 dependencies { implementation 'com.airbnb.android:mavericks:x.y.z' }
初始化(在 Application onCreate 时初始化):
1 2 3 4 5 6 class SampleApplication : Application () { override fun onCreate () { super .onCreate() **Mavericks.initialize(this )** } }
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 data class HelloWorldState (val title : String = "Hello World" ) : MavericksState class HelloWorldViewModel (initialState : HelloWorldState ) : MavericksViewModel <HelloWorldState >(initialState) { fun getMoreExcited () = setState { copy (title = "$title!" ) } }class HelloWorldFragment : Fragment (R.layout .hello_world_fragment ), MavericksView { private val viewModel : HelloWorldViewModel by fragmentViewModel () override fun invalidate () = withState (viewModel ) { state -> } fun initView ( ) { viewModel.onEach (HelloWorldState::title ) { title -> text_view.text = title } } }
数据流的流转过程如下:
通过 ViewModel 更新 State
ViewModel 接收数据
通过 setState 更新 state
MavericksState 发生变化后回调 invalidate 方法
通过 withState 获取 state,更新 UI
可以通过 onEach 监听 state 中某一个或多个属性的变化。
Mavericks 结合 Compose 1 💡 Mavericks + Compose = = Redux/Mobx + React
使用 Compose 构建 UI,Mavericks 可用于业务逻辑、数据获取、对象依赖注入等,有点类似于 Redux 和 React 的关系,Redux 用于数据管理和业务逻辑,React 组件负责构建 UI。
结合 compose 使用需要添加依赖:
1 implementation 'com.airbnb.android:mavericks-compose:x.y.z'
新建状态 和 ViewModel:
1 2 3 4 5 6 7 8 data class CounterState ( val count: Int = 0 , ) : MavericksStateclass CounterViewModel (initialState: CounterState) : MavericksViewModel<CounterState>(initialState) { fun incrementCount () = setState { copy(count = count + 1 ) } }
只需要在 Composable 函数里调用 mavericksViewModel() 来获取 ViewModel 实例。其默认关联的 LifecycleOwner 为 LocalLifecycleOwner,意味着该 LocalLifeyclerOwner 下的所有 Composable 将获取到相同的 ViewModel 实例。一般为 当前 Fragment 的 LifecycleOwner。如果使用 mavericksActivityViewModel 则 当前 ViewModel 的 LifecycleOwner 为 当前 Fragment 所在的 Activity,对于同一个 Activity,mavericksActivityViewModel 拿到的 都是同一个 ViewModel 实例。
1 2 3 4 5 6 7 @Composable fun IncrementNavigationCountButton () { val navScopedViewModel: CounterViewModel = mavericksViewModel() Button(onClick = navScopedViewModel::incrementCount) { Text("Increment Navigation Scoped Count" ) } }
要订阅 ViewModel 里的状态变化,Mavericks 提供了 ViewModel.collectAsState()。
1 2 3 4 5 6 7 val navScopedViewModel: CounterViewModel = mavericksViewModel()val activityScopedViewModel: CounterViewModel = mavericksActivityViewModel()val navScopedCount by navScopedViewModel.collectAsState(CounterState::count)val activityScopedCount by activityScopedViewModel.collectAsStateWithLifecycle(CounterState::count)
通过 属性委托的方式 ,获取到 State 中的状态和属性。要优化使用 collectAsState(),不同 Composable 访问不同的状态属性,在重组时候可以最大限度地减少状态更改时需要完成的工作。因此在 Composable 里订阅状态属性比订阅整个状态更理想。
核心源码探究 怎么检查是否是纯函数?
1 2 3 4 5 6 7 8 stateStore.set { val firstState = this .reducer() val secondState = this .reducer() if (firstState != secondState) { } firstState }
在 Mavericks 调试模式中,当调用 setState 时,会对 reducer 函数执行两次, 进行纯函数检查,如果不是 纯函数, 会抛出 IllegalArgumentException(仅调试模式)。
shared flow 还是 state flow?
SharedFlow 在 Mavericks 中被用来处理事件流和状态更新。
1 2 3 4 5 6 7 private val stateSharedFlow = MutableSharedFlow<S>( replay = 1 , extraBufferCapacity = SubscriberBufferSize, onBufferOverflow = BufferOverflow.SUSPEND, ).apply { tryEmit(initialState) } @InternalMavericksApi const val SubscriberBufferSize = 63
replay 参数指定了 SharedFlow 应该缓存多少个最新的值,并且在新的订阅者开始收集时重放这些值。这里设置为 1, 代表有新的订阅者时,会立即发送当前最新值,也就是这里的消息订阅是 “粘性的”。这减少了订阅者错过重要状态更新的风险。
extraBufferCapacity 参数指定了额外的缓冲区容量。SubscriberBufferSize是一个常量值 63,表示额外的缓冲区大小。这个缓冲区用于存储在订阅者还没有准备好处理新值时发送的值。这有助于避免丢失事件。是一个“背压”参数,背压(Backpressure)是指在处理数据流时,消费者无法跟上生产者的速度,从而导致数据积压的问题。当有慢速消费者时,这里会为其缓存状态。
BufferOverflow.SUSPEND 表示当缓冲区已满时,新的值将会被挂起,直到有空间可用。这确保了不会丢失任何事件,但可能会导致发送方挂起,直到缓冲区有空间。
sharedFlow 和 StateFlow 比较:
StateFlow 就是一个 replaySize=1 的 sharedFlow。同时它必须有一个初始值,此外,每次更新数据都会和旧数据做一次比较,只有不同时候才会更新数值。
StateFlow重点在状态 ,ui 永远有状态,所以 StateFlow 必须有初始值,同时对 ui 而言,过期的状态毫无意义,所以 stateFLow 永远更新最新的数据(和 liveData 相似),所以必须有粘滞度=1 的粘滞事件,让 ui 状态保持到最新。
SharedFlow侧重在事件 ,当某个事件触发,发送到队列之中,按照挂起或者非挂起、缓存策略等将事件发送到接受方,在具体使用时,SharedFlow 更适合通知 ui 界面的一些事件,比如 toast 等,也适合作为 viewModel 和 repository 之间的桥梁用作数据的传输。
结论:
这里 Mavericks 使用 SharedFlow 来 实现一个 StateFlow 用于 State 状态的 保存和更新。
withstate 和 setstate 怎么保证时序?
💡 viewModel 中的 withState 和 View 中的 withState 区别:
viewModel 中的 withState 是 异步的 block,通过 这个 withState 拿到的状态可以保证是最新的状态。View 中的 withState 是 同步的 block,直接执行 View 中的 withState 获取到的状态不能保证是最新的 state 状态。
我们应该尽量在 invalidate 或者 onEach、onAsync 环境中调用 View 中的 withState。 因为这三个方法都是在子线程中执行完 state 的变更后回调到主线程的,所以这里调 withState 获取到的肯定是最新的状态。
withState 队列 与 setState 队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private val setStateChannel = Channel<S.() -> S>(capacity = Channel.UNLIMITED)private val withStateChannel = Channel<(S) -> Unit >(capacity = Channel.UNLIMITED)private suspend fun flushQueuesOnce () { select<Unit > { setStateChannel.onReceive { reducer -> val newState = state.reducer() if (newState != state) { state = newState stateSharedFlow.emit(newState) } } withStateChannel.onReceive { block -> block(state) } } }
select 是一个 Kotlin 协程中的多路复用选择器,它允许在多个挂起函数中选择一个首先完成的函数。它的作用类似于 select 语句在 Go 语言中的作用。
这样保证了所有待处理 的 setState reducer 将在每一个 withState lambda 之前运行。
假设你有以下两个操作:
setState { ... }
withState { ... }
在调用 flushQueuesOnce 之前,setState 操作会被放入 setStateChannel 中,而 withState 操作会被放入 withStateChannel 中。flushQueuesOnce 函数会确保首先处理 setStateChannel 中的所有消息,然后再处理 withStateChannel 中的消息。这确保了在 withState 操作执行时,状态已经是最新的。
invalidate 效率问题探究
首先invalidate的思路本身没有问题,它和 React 中的render是保持一致的,但是 React 中是有 Virtual Dom 的 diff 算法来确定哪些节点需要真正更新的,如果没有这样的机制,就会造成效率问题。
在单纯 Mavericks 环境下,我们应该使用invalidate()吗?
我认为是不应该的,因为 Mavericks 没有这样的 diff 机制来保证局部刷新(可以通过 Airbnb 的 另一个框架 Epoxy 借助 Recyclerview 实现的 diff 机制来保证局部刷新,但是这又是另一回事了,在有了 Compose 之后,Epoxy 就显得有点鸡肋了)。所以我们在单纯 Mavericks 环境下应该尽量使用onEach来对指定的属性进行监听和订阅。
State 中便捷更新 List/Map 的扩展方法 list:
1 2 3 4 5 6 7 8 9 10 11 12 setState { copy(list = list - newList) } setState { copy(list = list + newList) }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) } setState { copy(list = list.update({ it.copy(checked = true ) }, { it.id == id })) }
map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fun <K, V> Map<K, V> .copy (vararg pairs: Pair <K , V>) = HashMap<K, V>(size + pairs.size).apply { putAll(this @copy ) pairs.forEach { put(it.first, it.second) } }fun <K, V> Map<K, V> .delete (vararg keys: K ) = HashMap<K, V>(size - keys.size).apply { this @delete .entries.asSequence() .filter { it.key !in keys } .forEach { put(it.key, it.value) } }
参考: Mavericks Docs (airbnb.io)
Mavericks Meet Jetpack Compose - 掘金 (juejin.cn)