Android MVI架构解析以及与其他架构对比
Posted slience....
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MVI架构解析以及与其他架构对比相关的知识,希望对你有一定的参考价值。
MVC
MVC架构主要分为以下几部分:
1.View: 对应于xm布局文件和java代码动态view部分。
2.Controller: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担功能过多。
3.Model: 主要负责网络请求,数据库处理,I/O操作,即页面的数据来源。
如2所说,android中xml布局功能性太弱,activity实际上负责了View层与Controller层两者的功能,所以在android的mvc变成了这样:
MVP
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
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。
DataBinding、ViewModel 和 LiveData 等组件是 Google 为了帮助我们实现 MVVM 模式提供的架构组件,它们并不是 MVVM 的本质,只是实现上的工具。
- Lifecycle: 生命周期状态回调;
- LiveData: 可观察的数据存储类;
- databinding: 可以自动同步 UI 和 data,不用再 findviewById();
- ViewModel: 存储界面相关的数据,这些数据不会在手机旋转等配置改变时丢失。
MVI
mvi的改动在于将View和ViewModel之间的多数据流改为基于ViewState的单数据流,MVI分为四个部分:
- View: Activity 和xml文件,与其他模式中的View的概念相同。
- Intent: 定义数据操作,将数据传到Model的唯一来源。
- ViewModel: 存储视图状态,负责处理表现逻辑,并将ViewState设置给可观察数据容器
- ViewState: 一个数据类,包含页面状态和对应的数据。
MVI特点
- 唯一可信源:数据只有一个来源(ViewModel),与MVVM思想相同
- 单向数据流:状态向下流动,事件向上流动。
- 响应式:ViewState包含页面当前状态和数据,View通过订阅ViewState就可以完成页面刷新。相比于 MVVM 是新的特性。
// 单数据流: View 和 ViewModel 之间只有一个数据流,只有一个地方可以修改数据,确保数据是安全稳定的。并且 View 只需要订阅一个 ViewState 就可以获取所有状态和数据,相比 MVVM 是新的特性;
响应式编程
响应式编程相对于命令式编程,
命令式编程:
val a = 1
val b = 2
var c = a + b // 3
a = 2
b = 2
c = a + b 执行完,后续c的值不会再改变,命令式编程是"一次性赋值"。
响应式编程:响应式编程是一种面向数据流和变化传播的声明式编程范式 “数据流”和“变化传播”是相互解释的:有数据流动,就意味着变化会从上游传播到下游。变化从上游传播到下游,就形成了数据流。
val flowA = MutableStateFlow(1)
val flowB = MutableStateFlow(2)
val flowC = flowA.combine(flowB) a, b -> a + b
coroutineScope.launch
flowC.collect
Log.v("ttaylor","c=$it")
coroutineScope.launch
delay(2000)
flowA.emit(2)
flowB.emit(2)
// 打印结果如下
// c=3
// c=4
单向数据流:
界面变化是数据流的末端,界面消费上游产生的数据,并随上游数据的变化进行刷新。
状态向下流动,事件向上流动的这种模式称为单向数据流
MVI强调数据的单向流动,主要分为几步:
1.用户操作以Intent的形式通知Model.
2.Model基于Intent更新State
3.View接收到State变化刷新UI
数据永远在一个环形结构中单向流动,不能反向流动。
缺点:
State 膨胀: 所有视图变化都转换为 ViewState,还需要管理不同状态下对应的数据。实践中应该根据状态之间的关联程度来决定使用单流还是多流;
内存开销: ViewState 是不可变类,状态变更时需要创建新的对象,存在一定内存开销;
局部刷新: View 根据 ViewState 响应,不易实现局部 Diff 刷新,可以使用 Flow#distinctUntilChanged() 来刷新来减少不必要的刷新。
Example:
MainActivity:
package com.lvlin.mvidemo.ui.view
@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity()
private lateinit var mainViewModel: MainViewModel
private var adapter = MainAdapter(arrayListOf())
private lateinit var buttonFetchUser: Button
private lateinit var recyclerview: RecyclerView
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonFetchUser = findViewById(R.id.buttonFetchUser)
recyclerview = findViewById(R.id.recyclerView)
progressBar = findViewById(R.id.progressBar)
setupUI()
setupViewModel()
observeViewModel()
setupClicks()
private fun setupUI()
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerview.run
addItemDecoration(
DividerItemDecoration(
recyclerview.context,
(recyclerview.layoutManager as LinearLayoutManager).orientation
)
)
recyclerview.adapter = adapter
private fun setupClicks()
buttonFetchUser.setOnClickListener
lifecycleScope.launch
mainViewModel.userIntent.send(MainIntent.FetchUser)
private fun setupViewModel()
mainViewModel = ViewModelProvider(
this,
ViewModelFactory(
ApiHelperImpl(
RetrofitBuilder.apiService
)
)
).get(MainViewModel::class.java)
private fun observeViewModel()
lifecycleScope.launch
mainViewModel.state.collect
when (it)
is MainState.Idle ->
is MainState.Loading ->
buttonFetchUser.visibility = View.GONE
progressBar.visibility = View.VISIBLE
is MainState.Users ->
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.GONE
renderList(it.user)
is MainState.Error ->
progressBar.visibility = View.GONE
buttonFetchUser.visibility = View.VISIBLE
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
private fun renderList(users: List<User>)
recyclerview.visibility = View.VISIBLE
users.let listofUsers -> listofUsers.let adapter.addData(it)
adapter.notifyDataSetChanged()
MainViewModel:
package com.lvlin.mvidemo.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lvlin.mvidemo.data.repository.MainRepository
import com.lvlin.mvidemo.ui.intent.MainIntent
import com.lvlin.mvidemo.ui.viewstate.MainState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import java.lang.Exception
/**
* @author: lvlin
* @email: lin2.lv@lvlin.com
* @date: 2022/7/12
*/
@ExperimentalCoroutinesApi
class MainViewModel(private val repository: MainRepository) : ViewModel()
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init
handleIntent()
private fun handleIntent()
viewModelScope.launch
userIntent.consumeAsFlow().collect
when (it)
is MainIntent.FetchUser -> fetchUser()
private fun fetchUser()
viewModelScope.launch
_state.value = MainState.Loading
_state.value = try
MainState.Users(repository.getUsers())
catch (e: Exception)
MainState.Error(e.localizedMessage)
MainState:
package com.lvlin.mvidemo.ui.viewstate
import com.lvlin.mvidemo.data.model.User
/**
* @author: lvlin
* @email: lin2.lv@lvlin.com
* @date: 2022/7/12
*/
sealed class MainState
object Idle : MainState()
object Loading : MainState()
data class Users(val user: List<User>) : MainState()
data class Error(val error: String) : MainState()
MainIntent:
package com.lvlin.mvidemo.ui.intent
/**
* @author: lvlin
* @email: lin2.lv@lvlin.com
* @date: 2022/7/12
*/
sealed class MainIntent
object FetchUser : MainIntent()
demo见github mvidemo
总结
- | -优点 | -缺点 |
---|---|---|
MVC | 职责划分 | vc耦合严重 |
MVP | 引入P层,解耦VC | 页面复杂时,接口增多。 |
MVVM | 引入VM,替代接口回调 | 数据流增多,livedata膨胀,模板代码增多 |
MVI | 借鉴前端框架,引入State,解决Livedata膨胀问题。响应式编程范式 | State膨胀,局部刷新,内存开销 |
**选择:**1.项目简单,未来改动也不大,不选择架构模式或方法,将模块封装好方便调用即可。
2.业务逻辑处理多的,mvp,mvvm都可以。
Android Jetpack系列之MVI架构
文章目录
写在前面
在之前介绍MVVM
的文章中,介绍了常用的MVC、MVP、MVVM
架构及其对MVVM
的封装使用,其中MVVM
的主旨可以理解为数据驱动:Repository
提供数据,ViewModel
中发送数据,UI层
使用的LiveData
订阅数据,当有数据变化时会主动通知UI层
进行刷新。有兴趣的可以去看一下:
1、 Android Jetpack系列之MVVM使用及封装
2、Android Jetpack系列之MVVM使用及封装(续)
那么MVI
又是什么呢?看了一些关于MVI
的文章,大家都称MVI是(Model-View-Intent)
,其中Intent
称为意图(注意这里的Intent
并不是页面跳转时使用的Intent
),MVI本质上是在MVVM
的基础上将View
与ViewModel
之间的数据传递做了统一整合。
google
官方文档中并没有MVI
的说法,而是在之前的MVVM
架构基础上进行了升级,其主旨意思与MVI
很相近,为了保持一致,后续介绍的MVVM
升级版架构统一称之为MVI
架构。
MVI vs MVVM
新旧架构对比
-
旧版
MVVM
架构:
-
新版
MVVM
或者称之为MVI
:
差异1、LiveData < T> 改为Flow< UIState>
关于LiveData
的缺点:
LiveData
的接收只能在主线程;LiveData
发送数据是一次性买卖,不能多次发送;LiveData
发送数据的线程是固定的,不能切换线程,setValue/postValue
本质上都是在主线程上发送的。当需要来回切换线程时,LiveData
就显得无能为力了。
Flow
可以完美解决LiveData遇到的问题,既可以多次从上游发送数据,也可以灵活地切换线程,所以如果涉及到来回切线程,那么使用Flow
是更优解。关于Flow
的详细用法,感兴趣的同学可以参见:Android Kotlin之Flow数据流
注:如果项目中还没有切换到Kotlin
,依然可以使用LiveData
来发送数据;如果已经切换到Kotlin
,那么更推荐使用Flow
来发送数据。
还有一点区别,LiveData
在旧版架构中传递的是单个实体数据,即每个数据都会对应一个LiveData
,很显然,如果页面逻辑很复杂的话,会导致ViewModel
中的LiveData
膨胀;新版架构中通过Flow
发送的统一为UIState
了,UIState
本质上也是一个data类
,不同的是UIState
会把View
层相关的实体状态统一管控,这样在ViewModel
中只需要一个Flow
来统一交互即可。
差异2、交互规范
新版架构中,提出了单向数据流来管理页面状态的概念:即数据的流向是固定的,整个数据流向是View -> ViewModel -> Model数据层 -> ViewModel获得数据 -> 根据UiState刷新View层
。其中,事件 Events
向上流动、状态 UiState
向下流动的。整体流程如下:
ViewModel
会存储并公开界面要使用的状态。界面状态是经过ViewModel
转换的应用数据。- 界面会向
ViewModel
发送用户事件通知。 ViewModel
会处理用户操作并更新状态。- 更新后的状态将反馈给界面以进行呈现。
- 系统会对导致状态更改的所有事件重复上述操作。
官方给了一个点击书签的示例:
上面是UI界面
中添加书签的操作,点击之后成功添加书签,那么整个数据的流转过程如下:
单向数据流提高了代码的可读性及修改的便利性。单向数据流有以下好处:
- 数据一致性。界面只有一个可信来源。
- 可测试性。状态来源是独立的,因此可独立于界面进行测试。
- 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。
MVI实战
示例图
定义UIState & 编写ViewModel
class MViewModel : BaseViewModel<MviState, MviSingleUiState>()
//Repository中间层 管理所有数据来源 包括本地的及网络的
private val mWanRepo = WanRepository()
override fun initUiState(): MviState
return MviState(BannerUiState.INIT, DetailUiState.INIT)
//请求Banner数据
fun loadBannerData()
requestDataWithFlow(
showLoading = true,
request = mWanRepo.requestWanData("") ,
successCallback = data ->
sendUiState
copy(bannerUiState = BannerUiState.SUCCESS(data))
,
failCallback =
)
//请求List数据
fun loadDetailData()
requestDataWithFlow(
showLoading = false,
request = mWanRepo.requestRankData() ,
successCallback = data ->
sendUiState
copy(detailUiState = DetailUiState.SUCCESS(data))
,
)
fun showToast()
sendSingleUiState(MviSingleUiState("触发了一次性消费事件!"))
/**
* 定义UiState 将View层所有实体类相关的都包括在这里,可以有效避免模板代码(StateFlow只需要定义一个即可)
*/
data class MviState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState?) : IUiState
data class MviSingleUiState(val message: String) : ISingleUiState
sealed class BannerUiState
object INIT : BannerUiState()
data class SUCCESS(val models: List<WanModel>) : BannerUiState()
sealed class DetailUiState
object INIT : DetailUiState()
data class SUCCESS(val detail: RankModel) : DetailUiState()
其中MviState
中定义的UIState
即是View
层相关的数据类,而MviSingleUiState
中定义的是一次性消费事件,如Toast
、跳转页面等,所以使用的Channel
来交互,在前面的文章中已经讲到了,这里不再重复。
相关接口:
interface IUiState //重复性事件 可以多次消费
interface ISingleUiState //一次性事件,不支持多次消费
object EmptySingleState : ISingleUiState
//一次性事件,不支持多次消费
sealed class LoadUiState
data class Loading(var isShow: Boolean) : LoadUiState()
object ShowMainView : LoadUiState()
data class Error(val msg: String) : LoadUiState()
LoadUiState
定义了页面加载的几种状态:正在加载Loading
、加载成功ShowMainView
、加载失败Error
,几种状态的使用与切换在BaseViewModel
中数据请求中进行了封装,具体使用可参考示例代码。- 如果页面请求中没有一次性消费事件,
ViewModel
初始化时可以直接传入EmptySingleState
。
基类BaseViewModel
/**
* ViewModel基类
*
* @param UiState 重复性事件,View层可以多次接收并刷新
* @param SingleUiState 一次性事件,View层不支持多次消费 如弹Toast,导航Activity等
*/
abstract class BaseViewModel<UiState : IUiState, SingleUiState : ISingleUiState> : ViewModel()
/**
* 可以重复消费的事件
*/
private val _uiStateFlow = MutableStateFlow(initUiState())
val uiStateFlow: StateFlow<UiState> = _uiStateFlow
/**
* 一次性事件 且 一对一的订阅关系
* 例如:弹Toast、导航Fragment等
* Channel特点
* 1.每个消息只有一个订阅者可以收到,用于一对一的通信
* 2.第一个订阅者可以收到 collect 之前的事件
*/
private val _sUiStateFlow: Channel<SingleUiState> = Channel()
val sUiStateFlow: Flow<SingleUiState> = _sUiStateFlow.receiveAsFlow()
private val _loadUiStateFlow: Channel<LoadUiState> = Channel()
val loadUiStateFlow: Flow<LoadUiState> = _loadUiStateFlow.receiveAsFlow()
protected abstract fun initUiState(): UiState
protected fun sendUiState(copy: UiState.() -> UiState)
_uiStateFlow.update _uiStateFlow.value.copy()
protected fun sendSingleUiState(sUiState: SingleUiState)
viewModelScope.launch
_sUiStateFlow.send(sUiState)
/**
* 发送当前加载状态: Loading、Error、Normal
*/
private fun sendLoadUiState(loadState: LoadUiState)
viewModelScope.launch
_loadUiStateFlow.send(loadState)
/**
* @param showLoading 是否展示Loading
* @param request 请求数据
* @param successCallback 请求成功
* @param failCallback 请求失败,处理异常逻辑
*/
protected fun <T : Any> requestDataWithFlow(
showLoading: Boolean = true,
request: suspend () -> BaseData<T>,
successCallback: (T) -> Unit,
failCallback: suspend (String) -> Unit = errMsg ->
//默认异常处理,子类可以进行覆写
sendLoadUiState(LoadUiState.Error(errMsg))
,
)
viewModelScope.launch
//是否展示Loading
if (showLoading)
sendLoadUiState(LoadUiState.Loading(true))
val baseData: BaseData<T>
try
baseData = request()
when (baseData.state)
ReqState.Success ->
sendLoadUiState(LoadUiState.ShowMainView)
baseData.data?.let successCallback(it)
ReqState.Error -> baseData.msg?.let
error(it)
catch (e: Exception)
e.message?.let failCallback(it)
finally
if (showLoading)
sendLoadUiState(LoadUiState.Loading(false))
基类中StateFlow
的默认值是通过initUiState()
来定义的,并强制需要子类实现:
override fun initUiState(): MviState
return MviState(BannerUiState.INIT, DetailUiState.INIT)
这样当一进入页面时就会在监听到这些初始化事件,并作出反应,如果不需要处理,可以直接略过。requestDataWithFlow
里封装了整个请求逻辑,
Repository数据支持
定义数据BaseData
类:
class BaseData<T>
@SerializedName("errorCode")
var code = -1
@SerializedName("errorMsg")
var msg: String? = null
var data: T? = null
var state: ReqState = ReqState.Error
enum class ReqState
Success, Error
基类BaseRepository:
open class BaseRepository
suspend fun <T : Any> executeRequest(
block: suspend () -> BaseData<T>
): BaseData<T>
val baseData = block.invoke()
if (baseData.code == 0)
//正确
baseData.state = ReqState.Success
else
//错误
baseData.state = ReqState.Error
return baseData
基类中定义请求逻辑,子类中直接使用:
class WanRepository : BaseRepository()
val service = RetrofitUtil.getService(DrinkService::class.java)
suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>>
return executeRequest service.getBanner()
suspend fun requestRankData(): BaseData<RankModel>
return executeRequest service.getRankList()
View层
/**
* MVI示例
*/
class MviExampleActivity : BaseMviActivity()
private val mBtnQuest: Button by id(R.id.btn_request)
private val mToolBar: Toolbar by id(R.id.toolbar)
private val mContentView: ViewGroup by id(R.id.cl_content_view)
private val mViewPager2: MVPager2 by id(R.id.mvp_pager2)
private val mRvRank: RecyclerView by id(R.id.rv_view)
private val mViewModel: MViewModel by viewModels()
override fun getLayoutId(): Int
return R.layout.activity_wan_android_mvi
override fun initViews()
initToolBar(mToolBar, "Jetpack MVI", true, true, BaseActivity.TYPE_BLOG)
mRvRank.layoutManager = GridLayoutManager(this, 2)
override fun initEvents()
registerEvent()
mBtnQuest.setOnClickListener
mViewModel.showToast() //一次性消费
mViewModel.loadBannerData()
mViewModel.loadDetailData()
private fun registerEvent()
/**
* Load加载事件 Loading、Error、ShowMainView
*/
mViewModel.loadUiStateFlow.flowWithLifecycle2(this) state ->
when (state)
is LoadUiState.Error -> mStatusViewUtil.showErrorView(state.msg)
is LoadUiState.ShowMainView -> mStatusViewUtil.showMainView()
is LoadUiState.Loading -> mStatusViewUtil.showLoadingView(state.isShow)
/**
* 一次性消费事件
*/
mViewModel.sUiStateFlow.flowWithLifecycle2(this) data ->
showToast(data.message)
mViewModel.uiStateFlow.flowWithLifecycle2(this, prop1 = MviState::bannerUiState) state ->
when (state)
is BannerUiState.INIT ->
is BannerUiState.SUCCESS ->
mViewPager2.visibility = View.VISIBLE
mBtnQuest.visibility = View.GONE
val imgs = mutableListOf<String>()
for (model in state.models)
imgs.add(model.imagePath)
mViewPager2.setIndicatorShow(true).setModels(imgs).start()
mViewModel.uiStateFlow.flowWithLifecycle2(this, Lifecycle.State.STARTED,
prop1 = MviState::detailUiState) state ->
when (state)
is DetailUiState.INIT ->
is DetailUiState.SUCCESS ->
mRvRank.visibility = View.VISIBLE
val list = state.detail.datas
mRvRank.adapter = RankAdapter().apply setModels(list)
override fun retryRequest()
//点击屏幕重试
mViewModel.showToast() //一次性消费
mViewModel.loadBannerData()
mViewModel.loadDetailData()
/**
* 展示Loading、Empty、Error视图等
*/
override fun getStatusOwnerView(): View?
return mContentView
先回看下新版架构图,View->ViewModel
请求数据时通过events
来进行传递,可以如在ViewModel
中进行封装:
sealed class EVENT : IEvent
object Banner : EVENT()
object Detail : EVENT()
override fun dispatchEvent(event: EVENT)
when (event)
EVENT.Banner -> loadBannerData()
EVENT.Detail -> loadDetailData()
那么View
层中可以如下调用:
mViewModel.dispatchEvent(EVENT.Banner)
mViewModel.dispatchEvent(EVENT.Detail)
而在示例中在View
层发送数据请求时,并没有在ViewModel
中将请求进行封装,而是直接通过mViewModel.loadBannerData()
进行的请求,个人认为封装Event
的做法有点多余了。
总结
升级版的MVI
架构相比于旧版MVVM
架构,规范性更好,约束性也更强。具体来说:
Flow
相比于LiveData
来说,能力更强,尤其当遇到来回切线程时;- 定义了
UIState
来集中管理页面的数据状态,从而ViewModel
中只需定义一个StateFlow
来管理即可,减少模板代码。同时定义UIState
也会带来副作用,即View
层没有diff
能力,会对每一次的事件进行全量更新,不过可以在View
层将UIState
里的内容细化监听来达到增量更新UI
的目的。
但是并不是说新版的架构就一定适合你的项目,架构毕竟是一种规范,具体使用还需要见仁见智。
完整示例代码
完整示例代码参见:MVI 示例
资料
【1】 应用架构指南:https://developer.android.com/jetpack/guide?hl=zh-cn
【2】界面层架构:https://developer.android.com/jetpack/guide/ui-layer?hl=zh-cn#views
【3】界面事件:https://developer.android.com/jetpack/guide/ui-layer/events?hl=zh-cn#views
以上是关于Android MVI架构解析以及与其他架构对比的主要内容,如果未能解决你的问题,请参考以下文章