Android Compose UI (二) 常规MVI操作

文章目录

      • 1.概述
      • 2.MVI的变化概括
      • 3.ViewModel的变化
      • 4.Activity/Fragment/View的变化
      • 5.Toast/Dialog/底部提示等临时UI状态
      • 6.总结

1.概述

随着Android应用不断的演化,从最开始的MVC->MVP->MVVM,现在Google官方也有了MVI的示例,相比较MVVM来说有了一些变化,接下来跟着文章一起了解.(其实这篇文章主要是介绍MVI的,和Compose UI没啥关系,但是因为后面一篇Compose UI + MVI),需要有前置了解MVI会更容易理解,所以有第二篇常规的MVI了解.

2.MVI的变化概括

Android Compose UI (二) 常规MVI操作_第1张图片

其实MVI和MVVM是类似的,因为MVI是在MVVM的基础上产生的演变.MVVM中我们会通过定义函数方法执行对应的点击逻辑,或通过databingding,或通过再Activity/Fragment中设置事件来触发,但是MVI中对于数据的流向变成了单向的,即MVVM中对LiveData是没有做限制的,因为适配了很多业务需要有很多数据LiveData来承载.MVI中事件通过Intent传入ViewModle后触发逻辑操作,再通过订阅反馈到界面,但是这里的一个订阅数据往往管理着多个状态,这样更有利于对数据状态的管理.

3.ViewModel的变化

class MVIViewModel : ViewModel() {

    val mainIntentChannel = Channel<MVI3Intent>(Channel.UNLIMITED)

    private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
    //只需要暴露一个LiveData,包括页面所有状态
    val viewStates: LiveData<Demo14ViewState> = _viewStates
    
    init {
        viewModelScope.launch {
            mainIntentChannel.consumeAsFlow().collect { value ->
               //接收到不同的意图后,去触发不同的逻辑,例如请求数据,保存数据等
            }
        }
    }
 
    
}


data class Demo14ViewState(
    val industrys: ArrayList<Industry> = arrayListOf(),
    val schools: ArrayList<SchoolBean> = arrayListOf(),
    var isChanged: Boolean = false
) : BaseViewState()

open class BaseViewState() {

}

  • 以上代码中mainIntentChannel作为Intent的意图收集通道,ViewModel接收到之后就可以去触发对应的逻辑操作,例如向云端请求数据之后反馈给View层,或者是保存数据之类的操作.
  • LiveData中存储的参数不再是一个Bean类或者是某一个集合数据,而且一个组合的UI状态集,其中包含了界面上面多个状态的数据集以及逻辑Flag
  • 在组合数据对象定义的时候就可以根据自己的业务去扩展需要的数据集,这里会产生一些疑问吧,都在一个对象中,怎么去更新数据呢? 一直操作这一个对象? 这里能够首先感受到 , 数据都维护到这个State对象中去了,等于后续需要什么都是从这个状态中获取,有了集中管理,看起来没有这么散乱了.
  • 这里把暴露出去的对象和ViewModel内部使用的Livedata做了区分,_viewStateviewState两个变量,viewState指定的类型是LiveData,这样外部就不可以再改变数据了,把数据逻辑处理都限制在了ViewModel内部
  • 最后我们再init块中,通过订阅接收View层传递的意图

4.Activity/Fragment/View的变化

页面中我们获取ViewModel还是和以前一样的,只不过在订阅数据和点击事件有了变化

class MVIActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.act_mvi)
		bTest.setOnClickListener {
           lifecycleScope.launch {
             	viewModel.mainIntentChannel.send(MVI3Intent.UpdateChange(false))
           		}
        	}
	}
}

sealed class MVI3Intent {

    //行为- 想要获取行业数据
    object GetIndustry : MVI3Intent()

    //行为- 想要获取学校数据
    sealed class GetSchool() : MVI3Intent()
    data class UpdateChange(var isChange: Boolean) : MVI3Intent()
}

点击事件中我们不再调用具体的函数,直接执行,而是通过刚才在ViewModel中定义的Channel去发送意图,这种定义在Intent中定义多种类型,对应不同的业务.(PS:这里的IntentAcitvityIntent不一样,不过也类似,因为也可以携带很多参数啥的)

override fun onCreate(savedInstanceState: Bundle?) {
   viewModel.viewStates.observeState(this, Demo14ViewState::industrys) { industrys ->
			//触发了内容改变
    }
}

fun <T, A> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner, prop1: KProperty1<T, A>, action: (A) -> Unit
) {
    this.map {
        WarpBean(prop1.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { a ->
        action.invoke(a.content1)
    }
}

data class WarpBean<T>(var content1: T)

在页面中我们再订阅数据的时候,可以通过obserState函数去订阅,这里我们做了扩展函数的封装,可以针对性的对想要的某一个数据进行订阅,比如实际业务中,数据集合含有滚动栏目,推荐栏目,商品栏目,小程序栏目,但是某个业务只依赖小程序的数据,就可以只订阅对象中的某一个数据,通过代理的形式.

override fun onCreate(savedInstanceState: Bundle?) {
    viewModel.viewStates.observeState(this, Demo14ViewState::industrys, Demo14ViewState::schools) { industrys, schools ->
            
  }
}


fun <T, A, B> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner, prop1: KProperty1<T, A>, prop2: KProperty1<T, B>, action: (A, B) -> Unit
) {
    this.map {
        WarpBean2(prop1.get(it), prop2.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { a ->
        action.invoke(a.content1, a.content2)
    }
}


data class WarpBean2<T, E>(var content1: T, var content2: E)

可以通过两个代理去实现订阅两个数据条目的方式,关注1个数据源还是2个数据源还是多个,还是整个,可以动态的根据业务的需求,灵活使用.

5.Toast/Dialog/底部提示等临时UI状态

在文章最开始在ViewModle中通过了LiveData去分发UI数据,但是LiveData会在页面重建旋转等场景下重新触发订阅,但是Toast提示和Dialog这种临时的弹窗是不需要再切换白天黑夜,旋转等重建场景下再次提示的,所以这里我们引入Flow管理,包括文章一开头的LiveData也可以通过Flow来实现

    private val _uiStates = MutableStateFlow<Demo14ViewState>(Demo14ViewState())
    val uiStates: StateFlow<Demo14ViewState> get() = _uiStates


    private val _uiEffect = MutableSharedFlow<UiEffect>()
    val uiEffect by lazy { _uiEffect.asSharedFlow() }

和文章一开始是一样的,对外暴露的是StateFlow以及ShardeFlow,但是ViewModel内部使用的时候还是使用的MutableStateFlowMutableSharedFlow,这里的uiStates就是用来反馈持久化的UI数据状态的,会在切换白天黑夜和屏幕旋转之类的场景重建再次订阅的时候接收到数据,uiEffect就是用来单次通知,例如Toast提示和弹窗提示等操作的时候,不会在重建的场景下订阅再次触发.

6.总结

MVI中,ViewModel不再有很多的被View层调用的函数方法,因为都通过Channel来统一接收了,也不再有很多的LiveData了,通过了数据的组合,再通过拆分数据订阅来实现业务,数据也统一管理了,这对封装来说有了更大的空间,可以限制子类的业务代码.但是实际业务使用还是要根据具体的业务来选择合适的架构.MVI有独有的优势,但是对应的数据管理相应的类也会增多.架构带来的层级越复杂,层数越多,可以扩展的地方也越多,但是后续迭代升级带来的复杂度也会越高,比如新加入开发人员上手难度会更大,但是如果业务的复杂度上来了,确实需要那完全是可以选择的.

你可能感兴趣的:(android,ui)