Android Jetpack系列之MVI架构

简介: 在之前介绍`MVVM`的文章中,介绍了常用的`MVC、MVP、MVVM`架构及其对`MVVM`的封装使用,其中`MVVM`的主旨可以理解为数据驱动:`Repository`提供数据,`ViewModel`中发送数据,`UI层`使用的`LiveData`订阅数据,当有数据变化时会主动通知`UI层`进行刷新。

写在前面

在之前介绍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的基础上将ViewViewModel之间的数据传递做了统一整合

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

MVI vs MVVM

新旧架构对比

  • 旧版MVVM架构:

MVVM

  • 新版MVVM或者称之为MVI

新版MVVM or 称之为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

相关文章
|
2月前
|
安全 Android开发 iOS开发
深入探索Android与iOS的差异:从系统架构到用户体验
在当今的智能手机市场中,Android和iOS无疑是最受欢迎的两大操作系统。本文旨在探讨这两个平台之间的主要差异,包括它们的系统架构、开发环境、安全性、以及用户体验等方面。通过对比分析,我们可以更好地理解为何不同的用户群体可能会偏好其中一个平台,以及这些偏好背后的技术原因。
|
2月前
|
Android开发 Swift iOS开发
深入探索iOS与Android操作系统的架构差异及其对应用开发的影响
在当今数字化时代,移动设备已经成为我们日常生活和工作不可或缺的一部分。其中,iOS和Android作为全球最流行的两大移动操作系统,各自拥有独特的系统架构和设计理念。本文将深入探讨iOS与Android的系统架构差异,并分析这些差异如何影响应用开发者的开发策略和用户体验设计。通过对两者的比较,我们可以更好地理解它们各自的优势和局限性,从而为开发者提供有价值的见解,帮助他们在这两个平台上开发出更高效、更符合用户需求的应用。
|
1月前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
1月前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统架构差异及其对开发者的影响
本文旨在通过对比分析iOS和Android两大移动操作系统的系统架构,探讨它们在设计理念、技术实现及开发者生态方面的差异。不同于常规摘要仅概述内容要点,本摘要将简要触及核心议题,为读者提供对两大平台架构特点的宏观理解,铺垫
|
1月前
|
网络协议 Linux Android开发
深入探索Android系统架构与性能优化
本文旨在为读者提供一个全面的视角,以理解Android系统的架构及其关键组件。我们将探讨Android的发展历程、核心特性以及如何通过有效的策略来提升应用的性能和用户体验。本文不包含常规的技术细节,而是聚焦于系统架构层面的深入分析,以及针对开发者的实际优化建议。
60 1
|
2月前
|
IDE 安全 Android开发
深入探索Android与iOS操作系统的架构差异
本文旨在对比分析Android和iOS两大主流移动操作系统在架构设计上的根本差异。通过详细解读两者的系统架构、开发环境、以及安全性等方面,揭示它们各自的特点及优势,为开发者选择合适的平台提供参考。
|
2月前
|
安全 Android开发 iOS开发
深入探讨Android与iOS的系统架构差异
本文旨在通过对比分析Android和iOS两大移动操作系统的系统架构,揭示它们在设计理念、安全性、应用生态及开发环境等方面的显著差异。我们将从底层架构出发,逐步剖析至用户界面层面,为开发者和科技爱好者提供一份详尽的技术参考。
43 1
|
30天前
|
开发工具 Android开发 iOS开发
Android与iOS生态差异深度剖析:技术架构、开发体验与市场影响####
本文旨在深入探讨Android与iOS两大移动操作系统在技术架构、开发环境及市场表现上的核心差异,为开发者和技术爱好者提供全面的视角。通过对比分析,揭示两者如何塑造了当今多样化的移动应用生态,并对未来发展趋势进行了展望。 ####
|
2月前
|
安全 Linux Android开发
深入探索Android与iOS的系统架构:一场技术较量
在当今数字化时代,智能手机操作系统的选择成为了用户和开发者关注的焦点。本文将深入探讨Android与iOS两大主流操作系统的系统架构,分析它们各自的优势与局限性,并对比两者在用户体验、开发生态和安全性方面的差异。通过本文的技术剖析,读者将对这两个平台的核心技术有更深入的理解。
|
2月前
|
安全 Java Linux
深入解析Android系统架构及其对开发者的意义####
【10月更文挑战第21天】 本文旨在为读者揭开Android操作系统架构的神秘面纱,探讨其如何塑造现代移动应用开发格局。通过剖析Linux内核、硬件抽象层、运行时环境及应用程序框架等关键组件,揭示Android平台的强大功能与灵活性。文章强调了理解Android架构对于开发者优化应用性能、提升用户体验的重要性,并展望了未来技术趋势下Android的发展方向。 ####
53 0