Android Jetpack系列之MVI架构

2023-10-28 09:40

本文主要是介绍Android Jetpack系列之MVI架构,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

      • 写在前面
      • MVI vs MVVM
        • 新旧架构对比
        • 差异1、LiveData < T> 改为Flow< UIState>
        • 差异2、交互规范
      • MVI实战
        • 示例图
        • 定义UIState & 编写ViewModel
        • Repository数据支持
        • View层
      • 总结
      • 完整示例代码
      • 资料

写在前面

在之前介绍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) : ISingleUiStatesealed 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(): UiStateprotected 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 {//是否展示Loadingif (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? = nullvar data: T? = nullvar 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.VISIBLEmBtnQuest.visibility = View.GONEval 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.VISIBLEval list = state.detail.datasmRvRank.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 Jetpack系列之MVI架构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/292354

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

mybatis的整体架构

mybatis的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

百度/小米/滴滴/京东,中台架构比较

小米中台建设实践 01 小米的三大中台建设:业务+数据+技术 业务中台--从业务说起 在中台建设中,需要规范化的服务接口、一致整合化的数据、容器化的技术组件以及弹性的基础设施。并结合业务情况,判定是否真的需要中台。 小米参考了业界优秀的案例包括移动中台、数据中台、业务中台、技术中台等,再结合其业务发展历程及业务现状,整理了中台架构的核心方法论,一是企业如何共享服务,二是如何为业务提供便利。

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动