MVI学习

文章

[[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 来承载 MVIModel 层,总体结构也与 MVVM 类似, 主要区别在于 ModelView 层交互的部分

  1. Model 层承载 UI 状态,并暴露出 ViewStateView 订阅,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 这一层可以加也可以不加, MavericksView 层与 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 通过 ActionViewModel 交互,通过 Action 通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控

缺点

MVI 也有一些缺点,比如

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

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

  • 使用 ViewStateState 集中管理,只需要订阅一个 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 是一个公开的只读属性,它通过调用 _viewStatesasLiveData() 方法返回一个不可变的 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/2024/04/a9ab203fdc2b.html
作者
六月的雨
发布于
2024年4月6日
许可协议