一、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刷新,是不是不合理?