我正在参与「启航方案」
写在前面
在之前介绍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
/**
* 一次性事情 且 1对1的订阅关系
* 例如:弹Toast、导航Fragment等
* Channel特点
* 1.每个音讯只要一个订阅者能够收到,用于1对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