android MVI架构

1、 Android Jetpack系列之MVVM使用及封装
2、Android Jetpack系列之MVVM使用及封装(续)

那么MVI又是什么呢?看了一些关于MVI的文章,大家都称MVI是(Model-View-Intent),其中Intent称为意图(注意这里的Intent并不是页面跳转时使用的Intent),MVI本质上是在MVVM的基础上将ViewViewModel之间的数据传递做了统一整合

google官方文档中并没有MVI的说法,而是在之前的MVVM架构基础上进行了升级,其主旨意思与MVI很相近,为了保持一致,后续介绍的MVVM升级版架构统一称之为MVI架构。

MVI vs MVVM

新旧架构对比
  • 旧版MVVM架构:

    android MVI架构_第1张图片

  • 新版MVVM或者称之为MVI

    android MVI架构_第2张图片

差异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 会处理用户操作并更新状态。
  • 更新后的状态将反馈给界面以进行呈现。
  • 系统会对导致状态更改的所有事件重复上述操作。

官方给了一个点击书签的示例:

android MVI架构_第3张图片

上面是UI界面中添加书签的操作,点击之后成功添加书签,那么整个数据的流转过程如下:

android MVI架构_第4张图片

单向数据流提高了代码的可读性及修改的便利性。单向数据流有以下好处:

  • 数据一致性。界面只有一个可信来源。
  • 可测试性。状态来源是独立的,因此可独立于界面进行测试。
  • 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。

MVI实战

示例图

android MVI架构_第5张图片

定义UIState & 编写ViewModel

kotlin

复制代码

class MViewModel : BaseViewModel() { //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) : BannerUiState() } sealed class DetailUiState { object INIT : DetailUiState() data class SUCCESS(val detail: RankModel) : DetailUiState() }

其中MviState中定义的UIState即是View层相关的数据类,而MviSingleUiState中定义的是一次性消费事件,如Toast、跳转页面等,所以使用的Channel来交互,在前面的文章中已经讲到了,这里不再重复。

相关接口


kotlin

复制代码

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


kotlin

复制代码

/** * ViewModel基类 * * @param UiState 重复性事件,View层可以多次接收并刷新 * @param SingleUiState 一次性事件,View层不支持多次消费 如弹Toast,导航Activity等 */ abstract class BaseViewModel : ViewModel() { /** * 可以重复消费的事件 */ private val _uiStateFlow = MutableStateFlow(initUiState()) val uiStateFlow: StateFlow = _uiStateFlow /** * 一次性事件 且 一对一的订阅关系 * 例如:弹Toast、导航Fragment等 * Channel特点 * 1.每个消息只有一个订阅者可以收到,用于一对一的通信 * 2.第一个订阅者可以收到 collect 之前的事件 */ private val _sUiStateFlow: Channel = Channel() val sUiStateFlow: Flow = _sUiStateFlow.receiveAsFlow() private val _loadUiStateFlow: Channel = Channel() val loadUiStateFlow: Flow = _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 requestDataWithFlow( showLoading: Boolean = true, request: suspend () -> BaseData, successCallback: (T) -> Unit, failCallback: suspend (String) -> Unit = { errMsg -> //默认异常处理,子类可以进行覆写 sendLoadUiState(LoadUiState.Error(errMsg)) }, ) { viewModelScope.launch { //是否展示Loading if (showLoading) { sendLoadUiState(LoadUiState.Loading(true)) } val baseData: BaseData 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()来定义的,并强制需要子类实现:


kotlin

复制代码

override fun initUiState(): MviState { return MviState(BannerUiState.INIT, DetailUiState.INIT) }

这样当一进入页面时就会在监听到这些初始化事件,并作出反应,如果不需要处理,可以直接略过。requestDataWithFlow里封装了整个请求逻辑,

Repository数据支持

定义数据BaseData类:


kotlin

复制代码

class BaseData { @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


kotlin

复制代码

open class BaseRepository { suspend fun executeRequest( block: suspend () -> BaseData ): BaseData { val baseData = block.invoke() if (baseData.code == 0) { //正确 baseData.state = ReqState.Success } else { //错误 baseData.state = ReqState.Error } return baseData } }

基类中定义请求逻辑,子类中直接使用:


kotlin

复制代码

class WanRepository : BaseRepository() { val service = RetrofitUtil.getService(DrinkService::class.java) suspend fun requestWanData(drinkId: String): BaseData> { return executeRequest { service.getBanner() } } suspend fun requestRankData(): BaseData { return executeRequest { service.getRankList() } } }

View层

kotlin

复制代码

/** * 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() 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中进行封装:


kotlin

复制代码

sealed class EVENT : IEvent { object Banner : EVENT() object Detail : EVENT() } override fun dispatchEvent(event: EVENT) { when (event) { EVENT.Banner -> { loadBannerData() } EVENT.Detail -> {loadDetailData() } }

那么View层中可以如下调用:


scss

复制代码

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的目的。

但是并不是说新版的架构就一定适合你的项目,架构毕竟是一种规范,具体使用还需要见仁见智。

作者:_小马快跑_
链接:https://juejin.cn/post/7156471865542721566
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(Android,android,架构)