MVI学习

文章

[[4.Flow学习]]

MVVM 进阶版:MVI 架构了解一下

MVI 架构更佳实践:Mavericks

响应式编程

unknown_filename.2|600

MVI 是在响应式编程的前提下,通过 “将页面状态聚合” 来统一消除上述 2 个问题,
也即原先分散在各个 LiveData 中的 String、Boolean 等状态,现全部聚合到一个 JavaBean / data class 中,由唯一的粘性观察者回推,所有控件都在该观察者中响应数据的变化。

MVI 架构为了解决 MVVM 在逻辑复杂时需要写多个 LiveData (可变+不可变) 的问题,使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态

通过集中管理 ViewState,只需对外暴露一个 LiveData,解决了 MVVM 模式下 LiveData 膨胀的问题

1
2
// 定义ViewState类
data class CounterViewState(val count: Int)

但页面的所有状态都通过一个 LiveData 来管理,也带来了一个严重的问题,即页面不支持局部刷新

虽说如果是 RecyclerView 可以通过 DifferUtil 来解决,但毕竟不是所有页面都是通过 RecyclerView 写的,支持 DifferUtil 也有一定的开发成本

因此直接使用 MVI 架构会带来一定的性能损耗,相信这是很多人不愿意用 MVI 架构的原因之一

MVI

unknown_filename|500
由于没有明确的状态管理标准,随着应用程序的增长或添加功能或事先没有计划的功能,视图渲染和业务逻辑可能会变得有点混乱,并且这种情况经常发生在 Android 应用开发过程中。可能你经常遇到状态管理导致业务逻辑和 UI 渲染的分工不明确,最终导致应用架构的混乱。而新提出的 MVI 架构,提倡一种单向数据流的设计思想,非常适合数据驱动型的 UI 展示项目。MVI 的架构思想来源于前端,由于 Model、View 和 Intent 三部分组成。

Model: 与其他 MVVM 中的 Model 不同的是,MVI 的 Model 主要指 UI 状态(State)。当前界面展示的内容无非就是 UI 状态的一个快照:例如数据加载过程、控件位置等都是一种 UI 状态
View: 与 MVvM 中的 View 一致,可能是一个 Activity、Fragment 或者任意 UI 承载单元。MVI 中的 View 通过订阅 Intent 的变化实现界面刷新
Intent: 此 Intent 不是 Activity 的 Intent,用户的任何操作都被包装成 Intent 后发送给 Model 进行数据请求

运用函数式编程思想将需求翻译成业务意图(I)、数据(M)、界面状态(V)间的函数关系,再用响应式编程的方式将其串联成数据流的过程。

个人感觉 mvi 和 mvvm 的区别就是 mvi 把 viewmodel 的方法改成了一个来统一处理,通过它的参数 intent 来区分不同调用。v 层也是一样,原来每个业务都有自己的 livedata,v 层需要对每一个 livedata 单独监听。如果使用 mvi 只需要使用一个 livedata 就可以了,利用 state 来区分不同的业务。

单向数据流

==MVI 强调数据的单向流动,主要分为以下几步==

==Intent 和 action 类似==

  1. 用户操作以 Intent 的形式通知 Model
  2. Model 基于 Intent 更新 State
  3. View 接收到 State 变化刷新 UI。
    数据永远在一个环形结构中单向流动,不能反向流动

我们使用 ViewModel 来承载 MVI 的 Model 层,总体结构也与 MVVM 类似, 主要区别在于 Model 与 View 层交互的部分

  1. Model 层承载 UI 状态,并暴露出 ViewState 供 View 订阅,ViewState 是个 data class,包含所有页面状态
  2. View 层通过 Action 更新 ViewState,替代 MVVM 通过调用 ViewModel 方法交互的方式。(Action 可以理解成 intent)
    image.png|600

实例

  • 大型页面使用,很多 liveData, 一俩个 liveData 不需要 MVi
  • 用户的任何操作都被包装成 Intent 发送给 Model 进行数据请求

大方向流程:

  1. view 点击调 viewModle 方法,把 intent(意图)发过去
  2. 通过 intent 更新 state(Model)
  3. view 接收到 State 变化刷新 UI。

Action 这一层可以加也可以不加, Mavericks 中 View 层与 Model 层的交互,也并没有包装成 Action,而是直接暴露的方法
上篇文章也的确有很多同学说使用 Action 交互比较麻烦,看起来 Action 这层的确可要可不要

尘埃落地 😛 遍历全网Android-MVI架构,学习总结一波 - 掘金

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

View 通过 Action 与 ViewModel 交互,通过 Action 通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控

缺点

MVI 也有一些缺点,比如

  1. 所有的操作最终都会转换成 State,所以当复杂页面的 State 容易膨胀
  2. state 是不变的,因此每当 state 需要更新时都要创建新对象替代老对象,这会带来一定内存开销

软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景, 读者可根据自己的需求选择使用。

  • 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态
  • 通过集中管理 ViewState,只需对外暴露一个 LiveData,解决了 MVVM 模式下 LiveData 膨胀的问题

但页面的所有状态都通过一个 LiveData 来管理,也带来了一个严重的问题,即页面不支持局部刷新
虽说如果是 RecyclerView 可以通过 DifferUtil 来解决,但毕竟不是所有页面都是通过 RecyclerView 写的,支持 DifferUtil 也有一定的开发成本
因此直接使用 MVI 架构会带来一定的性能损耗,相信这是很多人不愿意用 MVI 架构的原因之一

规范

1
2
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData() 
val viewStates = _viewStates.asLiveData()

这段代码是在使用 MVI(Model-View-Intent)架构模式时常见的一种写法。

首先,_viewStates 是一个私有的 MutableLiveData 对象,用于保存界面的状态。MutableLiveData 是 Android Jetpack 中的一个组件,用于在组件之间进行数据通信。

接着,viewStates 是一个公开的只读属性,它通过调用 _viewStates 的 asLiveData() 方法返回一个不可变的 LiveData 对象。LiveData 是一个被观察的数据持有者,可以通知观察者有关数据的变化。

在 MVI 架构中,viewStates 属性通常用于将界面的状态暴露给视图层(View Layer)。通过观察 viewStates 属性,视图层可以及时地获取最新的界面状态,并相应地更新用户界面。

为什么要使用这样的写法呢?MVI 架构的一个关键思想是单向数据流,即将界面的状态作为单一的源头,通过派发意图(Intents)和处理器(Reducers)来改变状态。通过将 _viewStates 声明为私有的 MutableLiveData,可以确保只有该类内部才能修改界面状态,而外部只能观察状态的变化。

此外,将 _viewStates 转换为不可变的 LiveData 对象可以保护数据的封装性,防止外部组件直接修改内部状态。

总结起来,这段代码的作用是在 MVI 架构中定义一个用于保存界面状态的私有 MutableLiveData 对象 _viewStates,并通过公开的只读属性 viewStates 将其暴露给视图层。这样可以确保界面状态的封装性和单向数据流的规范性。

密封类 sealed

Kotlin 中的 sealed 类是一种特殊类型的类,用于限制继承关系。当一个类被声明为 sealed 类时,它只能被其所在文件中的类继承,这意味着你无法在其他文件中继承它。

sealed 类常用于建模一组有限的类型,比如表示某个状态或者某个特定类型的集合。通过将这些相关的类型定义为 sealed 类的子类,可以在编译期间对所有可能的类型进行静态检查。

sealed 类与普通类的区别在于,必须将其所有可能的子类定义在同一文件中,且这些子类必须是 sealed 类的直接子类。这样做的好处是,在编译器能够检查出所有可能的子类,并且可以在 when 表达式中使用密封类作为分支条件,而不需要添加 else 分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sealed class Result
data class Success (val message: String) : Result ()
data class Error (val code: Int, val message: String) : Result ()

fun processResult (result: Result) {
when (result) {
is Success -> println ("Success: ${result. message}")
is Error -> println ("Error: ${result. code} - ${result. message}")
}
}

val success = Success ("Operation completed successfully")
val error = Error (404, "Resource not found")

processResult (success)
processResult (error)

在上述示例中,我们定义了一个 sealed 类 Result,并将它的两个可能的子类 Success 和 Error 定义在同一文件中。然后我们编写了一个函数 processResult,根据传入的 Result 对象进行处理。通过使用 when 表达式,我们能够对 Result 类型的不同子类进行分支处理。

总而言之,Kotlin 中的 sealed 类提供了一种方便的方式来建模有限类型集合,并在编译期间进行类型检查,使代码更加安全和可靠。


例子

下面是 chatGPT 给的例子

1. 创建一个 Model 类:

1
data class CounterModel (val count: Int)

2. 创建一个 Intent 类:

1
2
3
4
5
sealed class CounterIntent {
object IncrementIntent : CounterIntent ()
object DecrementIntent : CounterIntent ()
}

3. 创建一个 ViewState 类:

1
2
data class CounterViewState (val count: Int)

4. 创建一个 ViewModel 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CounterViewModel : ViewModel () {
private val _state = MutableLiveData<CounterViewState>()
val state: LiveData<CounterViewState>
get () = _state

init {
_state. value = CounterViewState (0)
}

fun processIntent (intent: CounterIntent) {
val currentState = _state. value ?: CounterViewState (0)

when (intent) {
is CounterIntent. IncrementIntent -> {
_state. value = currentState.copy (count = currentState. count + 1)
}
is CounterIntent. DecrementIntent -> {
_state. value = currentState.copy (count = currentState. count - 1)
}
}
}
}

5. 在 Activity 或 Fragment 中使用 ViewModel:

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
class CounterActivity : AppCompatActivity () {

private lateinit var viewModel: CounterViewModel

override fun onCreate (savedInstanceState: Bundle?) {
super.onCreate (savedInstanceState)
setContentView (R.layout. activity_counter)

viewModel = ViewModelProvider (this). get (CounterViewModel:: class. java)

viewModel.state.observe (this, Observer { state ->
render (state)
})

buttonIncrement. setOnClickListener {
viewModel.processIntent (CounterIntent. IncrementIntent)
}

buttonDecrement. setOnClickListener {
viewModel.processIntent (CounterIntent. DecrementIntent)
}
}

private fun render (state: CounterViewState) {
textViewCount. text = state.count.toString ()
}
}

在这个示例中,我们使用了 ViewModel 来处理业务逻辑和状态管理,同时使用 LiveData 来观察状态的变化。通过定义 Model、Intent、ViewState 来区分数据模型、用户操作意图、界面状态,实现了简单的 MVI 架构。希望这个例子能帮助你更好地理解如何在 Android 应用中使用 MVI 架构。


MVI学习
http://peiniwan.github.io/2025/12/35c15b4003bd.html
作者
六月的雨
发布于
2025年12月16日
许可协议