注册

MVI 架构的理解

回顾MVC MVP MVVM


MVC


image.png


MVC架构主要分为以下几部分:




  • View层: 对应于xm布局文件和java代码动态view部分。




  • Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担功能过多。




  • Model层: 主要负责网络请求,数据库处理,I/O操作,即页面的数据来源。




MVC数据流向为:



  • View接收用户的点击
  • View请求Controller进行处理或直接去Model获取数据
  • Controller请求model获取数据,进行其他的业务操作,将数据反馈给View层

MVC缺点:


如上2所说,android中xml布局功能性太弱,activity实际上负责了View层与Controller层两者的功能,耦合性太高。


MVP:


image.png


MVP主要分为以下几部分:


1.View层:对应于Activity与xml,只负责显示UI,只与Presenter层交互,与Model层没有耦合。


2.Presenter层:主要负责处理业务逻辑,通过接口回调View层。


3.Model层:主要负责网络请求,数据库处理的操作。


MVP解决了MVC的两个问题,即Activity承担了两层职责与View层和Model层耦合的问题。


MVP缺点:


1.Presenter层通过接口与View通信,实际上持有了View的引用。


2.业务逻辑的增加,一个页面变得复杂,造成接口很庞大。


MVVM


image.png
MVVM改动在于将Presenter改为ViewModel,主要分为以下几部分:


1.View: Activity和Xml,与其他的相同


2.Model: 负责管理业务数据逻辑,如网络请求,数据库处理,与MVP中Model相同


3.ViewModel:存储视图状态,负责处理表现逻辑,并将数据设置给可观察容器。


View和Presenter从双向依赖变成View可以向ViewModel发送指令,但ViewModel不会直接向View回调,而是让View通过观察者的模式去监听数据的改变,有效规避MVP双向依赖的缺点。


MVVM缺点:


多数据流:View与ViewModel的交互分散,缺少唯一修改源,不易于追踪。


LiveData膨胀:复杂的页面需要定义多个MutableLiveData,并且都需要暴露为不可变的LivewData。


MVI是什么?


先上图


image.png
其主要分为以下几部分




  1. Model层: 与MVVM中的Model不同的是,MVIModel可以理解是View Model,存储视图状态,负责处理表现逻辑,并将ViewState设置给可观察数据容器




  2. View层: 与其他MVVM中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新




  3. Intent层: 此Intent不是ActivityIntent,而是指用户的意图,比如点击加载,点击刷新等操作,用户的任何操作都被包装成Intent,在model层观察用户意图从而去做加载数据等操作。




目前android主流的MVI是基于协程+flow+viewModel去实现的,协程应该大家都知道,所以先来了解一下MVI中的flow


flow是什么?


在flow 中,数据如水流一样经过上游发送,中间站处理,下游接收,类似于Rxjava,使用各种操作符实现异步数据流框架
代码示例:


   runBlocking {
flow {
emit(1)
emit(2)
emit(3)
emit(4)
emit(5)
}.filter { it > 2 }
.map { it * 2 }
.take(2)
.collect {
Log.d("FLOW", it.toString())
}
}

flow是冷流,只有订阅者订阅时,才开始执行发射数据流的代码
即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据,从上述例子看,当调用了collect后,才会执行flow语句块里面的代码,并且flow每次重新订阅收集都会将所有事件重新发送一次


但是在我们的开发场景中,一般是先触发某个事件(比如请求数据之后)才会去刷新UI,显然flow不适用于这种场景,因为flow只有在下游开始消费时才会触发生产数据


因此引入一个新的概念,StateFlow:


StateFlow与Flow的区别是StateFlow是热流,即无论下游是否有消费行为,上游都会自己产生数据。
代码示例:


在ViewModel创建StateFlow,发送UI状态,关于UI状态下面会讲,这里主要了解StateFlow的用法


//创建flow
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

//发送UI状态
state.value = ViewState.Loading

在Activity中接收:


    mViewModel.state.collect{
when(it) {
is ViewState.Default -> {

}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}

看起来这个StateFlow用法和MVVM中的LiveData类似,那它们有什么区别呢?


区别1:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。


区别2:当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,停止发送数据, 而从 StateFlow 收集数据的操作并不会自动停止。如需要实现LiveData相同的行为,可以在 Lifecycle.repeatOnLifecycle 块中去观察数据流。


MVI框架构建:


Intent介绍:


上面提到,intent指的是用户意图,在代码层面上来说他其实就是个枚举类,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用写else


sealed class UserIntent {
object GetBanners: UserIntent() //定义了一个用户获取Banner的意图
}

处理Intent


这里需要了解一下:Chnnel


channel主要用于协程之间的通讯,使用send和receive往通道里写入或者读取数据,2个方法为非阻塞挂起函数,channel是热流,不管有没有订阅者都会发送。
我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
根据上面的例子,用Channel把UserIntent处理一下:
在View Model定义并观察用户意图:


class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>() //定义用户意图

init {
observeUserIntent()
}

private fun observeUserIntent() { //观察用户意图
viewModelScope.launch {
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

Activity中发送用户意图:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() { //将用户意图传给view Model
lifecycleScope.launch {
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

看完上面的代码,MVI中的View到Model之间的数据流向就已经清晰了,
接下来就是Model向View层传递数据的过程


State介绍:


State是UI状态,MVI的一个特点就是数据状态统一管理,state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
定义一个State类:


sealed class ViewState {
object Default: ViewState() //页面默认状态
object Loading : ViewState() //页面加载
data class BannerMsg(val data: List<Banner>?): ViewState() //页面加载完成
data class Error(val error: String?): ViewState() //页面加载错误
}

处理State


在ViewModel中观测到用户意图,根据用户意图去做相关操作,然后将UI State反馈给用户



class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>()
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

init {
observeUserIntent()
}

private fun observeUserIntent() {
viewModelScope.launch { //观测用户意图
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

private fun loadBanner() {
viewModelScope.launch {
state.value = ViewState.Loading //加载中状态 反馈给View层
val banners = CloudService.cloudApi.getBanner() //获取数据
banners.data?.let {
state.value = ViewState.BannerMsg(it) //加载成功状态,数据反馈给View
return@launch
}
state.value = ViewState.Error(banners.errorMsg) //加载错误状态反馈给View
}
}
}

Activity中观察页面状态:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
observeViewModel()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() {
lifecycleScope.launch {
//发送用户意图
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
mViewModel.state.collect{ //观测UI状态,根据不同的状态刷新Ui
when(it) {
is ViewState.Default -> {
//初始值不做任何操作
}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}
}
}
}

MVI架构主要代码介绍完毕。


MVI总结:


MVI强调数据的单向流动,主要分为几步:




  • 用户操作以Intent的形式通知Model.




  • Model基于Intent更新State




  • View接收到State变化刷新UI




数据永远在一个环形结构中单向流动,不能反向流动。


MVI优缺点


优点:



  • MVI的核心思想是 view-intent-viewmodel-state-view 单向数据流,MVVM核心思想是 view-viewmodel-view 双向数据流


    • 代码分层更清晰,viewmodel 无需关心view如何触发和更新,只需要维护intentstate即可




    • IntentState的引入解决了ViewModelModel的界限模糊问题



缺点:




  • 单向流和双向流,并非 好和不好 的选择,而是 适合和不适合 的选择,业务逻辑较为简单的界面,它不需要mvi、mvvm、mvp、mvc,只一个activity或者fragment + layout 即可,一味的套用架构,反而适得其反!




  • 逻辑、数据、UI 较为复杂时,intentstate将会变得臃肿


作者:用户1577096808602
链接:https://juejin.cn/post/7222460499493568571
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册