一、MVI模式
MVVM开发模式最大的弊端就是大量的观察者,大量的LiveData模板代码,参考文档1
页面有多少种状态,就要定义多少个类似的变量,模板代码过多,且不利于维护
文章中用的Flow持有页面数据,和LiveData类似,针对上面这个缺点,文章重新定义了MainUiState,这里的UiState定义成了密封类,这个定义也就是说,页面的状态就是一个状态机,很显然不准确,一个页面应该是多种状态机的组合,但是如果定义多个UiState,那又回到了LiveData的模板代码。
所以最佳的UiState实践应该是下面这种实现 参考文档2
文章中将UiState定义为data class,一个页面持有多个数据变量,这是比较合理的页面状态。但是文章中又提出Effect的概念,Effect和Intent的边界实在模糊,没必要分太清楚,一个Intent就可以应对。
二、MVI-Demo
可以从一个简单的demo入手,来看下实践代码。
1.首先定义ViewModel的基类,主要是ViewModel中持有STATE数据
open class BaseViewModel<STATE, INTENT>(application: Application) : AndroidViewModel(application), ViewModelContract<INTENT> {
private val _uiStatesLiveData: SingleLiveEvents<STATE> = SingleLiveEvents()
fun uiStatesLiveData(): LiveData<STATE> = _uiStatesLiveData
private var _uiState: STATE? = null
protected var uiState: STATE
get() = _uiState ?: throw UninitializedPropertyAccessException("init error")
set(value) {
_uiState = value
_uiStatesLiveData.value = value
}
override fun dispatch(intent: INTENT) {
}
}
2.定义BaseActivity的基类,主要是监听UiState的变化,当变化的时候调用子类复写的方法,通知子类刷新页面
abstract class BaseActivity<STATE, INTENT, VM : BaseViewModel<STATE, INTENT>, VB : ViewDataBinding> : AppCompatActivity() {
abstract val viewModel: VM
lateinit var viewBinding: VB
private val mUiStateObserver = Observer<STATE> { renderViewState(it) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = DataBindingUtil.setContentView(this, layoutId())
viewModel.uiStatesLiveData().observe(this, mUiStateObserver)
renderView()
renderListener()
}
abstract fun layoutId(): Int
open fun renderView(){
}
open fun renderListener(){
}
abstract fun renderViewState(uiState: STATE)
}
3.主页就三个TextView,三个Button,当Button点击的时候,更新一下对应TextView的文字,非常简单的功能,所以可以快速定义出主页的协议类
data class MainUiState(
var title1: String = "",
var title2: String = "",
var title3: String = "",
)
sealed class MainIntent{
object ClickButton1Intent: MainIntent()
object ClickButton2Intent: MainIntent()
object ClickButton3Intent: MainIntent()
}
看代码就知道UiState持有三个title变量,当点击按钮的时候发出三个Intent。
4.实现MainViewModel,把UiState实例化出来,对收到的Intent作出响应
class MainViewModel(application: Application) : BaseViewModel<MainUiState, MainIntent>(application) {
init {
uiState = MainUiState()
}
override fun dispatch(intent: MainIntent) {
when (intent) {
is MainIntent.ClickButton1Intent -> {
clickButton1()
}
is MainIntent.ClickButton2Intent -> {
clickButton2()
}
is MainIntent.ClickButton3Intent -> {
clickButton3()
}
}
}
private fun clickButton3() {
uiState = uiState.copy(title3 = "点击了按钮3")
}
private fun clickButton2() {
uiState = uiState.copy(title2 = "点击了按钮2")
}
private fun clickButton1() {
uiState = uiState.copy(title1 = "点击了按钮1")
}
}
5.最后,MainActivity的代码就非常简短了
class MainActivity : BaseActivity<MainUiState, MainIntent, MainViewModel, ActivityMainBinding>() {
override val viewModel: MainViewModel by viewModels()
override fun layoutId(): Int {
return R.layout.activity_main
}
override fun renderView() {
}
override fun renderListener() {
viewBinding.btn1.setOnClickListener {
viewModel.dispatch(MainIntent.ClickButton1Intent)
}
viewBinding.btn2.setOnClickListener {
viewModel.dispatch(MainIntent.ClickButton2Intent)
}
viewBinding.btn3.setOnClickListener {
viewModel.dispatch(MainIntent.ClickButton3Intent)
}
}
override fun renderViewState(uiState: MainUiState) {
viewBinding.tvTxt1.text = uiState.title1
viewBinding.tvTxt2.text = uiState.title2
viewBinding.tvTxt3.text = uiState.title3
}
}
所有对UI的修改最终都会走到renderViewState方法中,处理逻辑全部在ViewModel中,UI和逻辑隔离开,耦合度降低。代码的可读性和维护性都很高,但是有一个最大的问题,就是如果只点击了Button1,TextView2和TextView3全部setText了一遍,这只是Demo,实际开发中,UI的逻辑比这个复杂多了,随便修改一个UiState的属性,整个页面都会刷新一遍,比如更新标题结果导致ListView刷新,是不是不合理?