翻译自 https://proandroiddev.com/mvi-architecture-with-kotlin-flows-and-channels-d36820b2028d

MVVM是许多开发者推荐使用的架构,但是就像其他事情一样,架构的模式也在不停的进化中

MVI是MVx家族中最新的成员,它和MVVM具有很多共同点,但是在状态管理上有更加结构化的方式

MVI聚焦三个部分 Model-View-Intent

  • Model 代表UI的状态,比如一个UI可能有Idle、Loading、Loaded这些状态
  • View 从ViewModel和updateUI来设置不可变的状态
  • Intent 不是传统Android概念中的intent,它代表用户和UI交互的意图,比如点击一个按钮

现在让我们看下code

1…

首先我们需要创建一些描述类型的接口

1
2
3
4
5
6
7
8
 //当前views的状态
interface UiState

//用户的actions
interface UiEvent

//仅展示1次的副作用(side effects) 比如错误信息
interface UiEffect

现在我们创建一个ViewModel的基础类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
abstract class BaseViewModel<Event : UiEvent, State : UiState, Effect : UiEffect> : ViewModel() {

// Create Initial State of View
private val initialState : State by lazy { createInitialState() }
abstract fun createInitialState() : State

// Get Current State
val currentState: State
get() = uiState.value

private val _uiState : MutableStateFlow<State> = MutableStateFlow(initialState)
val uiState = _uiState.asStateFlow()

private val _event : MutableSharedFlow<Event> = MutableSharedFlow()
val event = _event.asSharedFlow()

private val _effect : Channel<Effect> = Channel()
val effect = _effect.receiveAsFlow()

}

StateFlow-SharedFlow-Channel的不同

SharedFlow,事件可以被广播给未知数量的订阅者(0或者更多),在没有订阅者的情况下,任何发布的事件将会被立即丢弃。它是一种用于必须立即处理或者不处理事件的设计模式

channel,每个事件只能传给单个订阅者,在没有订阅者情况下发布事件,将在通道缓冲区满的时候暂停,等待订阅者出现。默认情况下发布的事件不会被丢弃

更多不同可以参考

对于处理UIState 我们使用 StateFlowStateFlow就像LiveData,只不过具有初始值,因此我们始终具备初始状态,它也是一种类型的ShareFlow.我们通常想要接收当UI可见后的view状态

对于处理UiEvent,我们使用SharedFlow.我们考虑的是当没有接收者的时候就放弃这个事件

最后对于处理UiEffect我们使用Channels,因为Channels是hot,我们不需要再次展示side effect,当方向改变或者UI再次变得可见。简而言之就是我们想复制SingleEvent的行为

2…

然后我们需要UiState、UiEvent和UiEffect的set方法

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
 /**
* Set new Event
*/
fun setEvent(event : Event) {
val newEvent = event
viewModelScope.launch { _event.emit(newEvent) }
}


/**
* Set new Ui State
*/
protected fun setState(reduce: State.() -> State) {
val newState = currentState.reduce()
_uiState.value = newState
}

/**
* Set new Effect
*/
protected fun setEffect(builder: () -> Effect) {
val effectValue = builder()
viewModelScope.launch { _effect.send(effectValue) }
}

为了处理Events我们需要处理eventFlow在ViewModel的init方法区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
init {
subscribeEvents()
}

/**
* Start listening to Event
*/
private fun subscribeEvents() {
viewModelScope.launch {
event.collect {
handleEvent(it)
}
}
}

/**
* Handle each event
*/
abstract fun handleEvent(event : Event)

3…

我们看下MainContract,这个是连接MainActivity和MainViewModel的纽带

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
 
class MainContract {

// Events that user performed
sealed class Event : UiEvent {
object OnRandomNumberClicked : Event()
object OnShowToastClicked : Event()
}

// Ui View States
data class State(
val randomNumberState: RandomNumberState
) : UiState

// View State that related to Random Number
sealed class RandomNumberState {
object Idle : RandomNumberState()
object Loading : RandomNumberState()
data class Success(val number : Int) : RandomNumberState()
}

// Side effects
sealed class Effect : UiEffect {

object ShowToast : Effect()

}

}

我们只有2个事件,OnRandomNumberClicked事件会在用户点击随机生产数按钮时候被发射出来,并且这里有个简单的toast按钮,模拟SingleLiveEvent的行为

RandomNumberState是一个sealed类,它持有不同的状态包括Idle, Loading and Success

State是一个简单的数据类,代表UI元素的状态

Effect是一个简单的行为,主要是我们想要展示一次的行为

在view state中我们不需要使用sealed class,我们只需要使用如下简单的变量

1
2
3
4
5
data class State(
val isLoading: Boolean = false,
val randomNumber: Int = -1,
val error: String? = null
) : UiState

4…

以上我们完成了MainContract,我们再看下处理实际逻辑的MainViewModel

1
2
3
4
5
6
7
8
 /**
* Create initial State of Views
*/
override fun createInitialState(): MainContract.State {
return MainContract.State(
MainContract.RandomNumberState.Idle
)
}

我们需要创建初始化views,在这个case里面,他就是一个空状态

1
2
3
4
5
6
7
8
9
10
11
12
 /**
* Handle each event
*/
override fun handleEvent(event: MainContract.Event) {
when (event) {
is MainContract.Event.OnRandomNumberClicked -> { generateRandomNumber() }
is MainContract.Event.OnShowToastClicked -> {
setEffect { MainContract.Effect.ShowToast }
}
}
}

我们在handleEvent中处理每个handle,我们在contract添加的每一个event都需要在这里添加,这样event可以在同一地方集中管理

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
/**
* Generate a random number
*/
private fun generateRandomNumber() {
viewModelScope.launch {
// Set Loading
setState { copy(randomNumberState = MainContract.RandomNumberState.Loading) }
try {
// Add delay for simulate network call
delay(5000)
val random = (0..10).random()
if (random % 2 == 0) {
// If error happens set state to Idle
// If you want create a Error State and use it
setState { copy(randomNumberState = MainContract.RandomNumberState.Idle) }
throw RuntimeException("Number is even")
}
// Update state
setState { copy(randomNumberState = MainContract.RandomNumberState.Success(number = random)) }
} catch (exception : Exception) {
// Show error
setEffect { MainContract.Effect.ShowToast }
}
}
}

每次OnRandomNumberClicked事件触发时候都会调用generateRandomNumber方法;从Loading state开始,然后根据结果将状态更改为Success或Idle;根据你的用例,希望创建一个Error状态,并在number为偶数时设置它,当error出现时我们设置一个effect,然后显示一个toast

5…

最后,我们需要在UI展示View 的状态

1
2
3
4
5
6
binding.generateNumber.setOnClickListener {
viewModel.setEvent(MainContract.Event.OnRandomNumberClicked)
}
binding.showToast.setOnClickListener {
viewModel.setEvent(MainContract.Event.OnShowToastClicked)
}

每次我们点击一个按钮,将会发射出一个对应的事件

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
// Collect ui state
lifecycleScope.launchWhenStarted {
viewModel.uiState.collect {
when (it.randomNumberState) {
is MainContract.RandomNumberState.Idle -> { binding.progressBar.isVisible = false }
is MainContract.RandomNumberState.Loading -> { binding.progressBar.isVisible = true }
is MainContract.RandomNumberState.Success -> {
binding.progressBar.isVisible = false
binding.number.text = it.randomNumberState.number.toString()
}
}
}
}

// Collect side effects
lifecycleScope.launchWhenStarted {
viewModel.effect.collect {
when (it) {
is MainContract.Effect.ShowToast -> {
binding.progressBar.isVisible = false
// Simple method that shows a toast
showToast("Error, number is even")
}
}
}
}

我们通过collect uistate来刷新UI;为了模仿LiveData的行为,我们通过使用launchWhenStarted来collect flow

6…

我们总结下app的flow,它是十分简单的,首先,我们触发一个与用户操作(如按钮点击)相关的Event。 然后,作为这个事件的结果,我们设置了一个新的不可变状态。 此状态包括空闲、加载或成功。 因为我们使用StateFlow,所以一旦有了新的状态,UI就会更新。

如果有一个错误,需要显示一次消息,如吐司或警告对话框,我们设置一个新的Effect

7…

优势

  • State对象是不可变的,因此它是线程安全的
  • 所有的行为,像state、event、effect在同一个文件中,因此比较容易理解应用的行为
  • 维护状态也比较简单
  • 由于数据流是单向的,因此追踪起来也比较容易

劣势

  • 引入了很多模板文件
  • 由于我们创建了很多文件,因此带来了比较高的内存管理成本
  • 有时,我们有很多views和复杂的逻辑,在这种情形下State变得臃肿,我们可能想要通过其他的StateFlow进一步切分State到更小的单元

8…

MVI是MVx家族的最后一个成员。 它与MVVM有很多共同之处,但是有更结构化的状态管理方式。 在本文中,我们介绍了MVI模式,并给出了一个简单的实现。 您可以在这里下载完整的源代码

你也可以参考另一篇非常棒的文章here其中提出了另一个很棒的框架orbit-mvi