MVI架构理解

news2024/11/15 14:06:20

回顾MVC MVP MVVM

MVC

image.png

MVC架构主要分为以下几部分:

  • View层: 对应于xm布局文件和java代码动态view部分。

  • Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担功能过多。

  • Model层: 主要负责网络请求,数据库处理,I/O操作,即页面的数据来源。

MVC数据流向为:

  • View接收用户的点击
  • View请求Controller进行处理或直接去Model获取数据
  • Controller请求model获取数据,进行其他的业务操作,将数据反馈给View层

MVC缺点:

如上2所说,android中xml布局功能性太弱,activity实际上负责了View层与Controller层两者的功能,耦合性太高。

MVP:

image.png

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

image.png
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。

MVI是什么?

先上图

image.png
其主要分为以下几部分

  1. Model层: 与MVVM中的Model不同的是,MVIModel可以理解是View Model,存储视图状态,负责处理表现逻辑,并将ViewState设置给可观察数据容器

  2. View层: 与其他MVVM中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新

  3. Intent层: 此Intent不是ActivityIntent,而是指用户的意图,比如点击加载,点击刷新等操作,用户的任何操作都被包装成Intent,在model层观察用户意图从而去做加载数据等操作。

目前android主流的MVI是基于协程+flow+viewModel去实现的,协程应该大家都知道,所以先来了解一下MVI中的flow

flow是什么?

在flow 中,数据如水流一样经过上游发送,中间站处理,下游接收,类似于Rxjava,使用各种操作符实现异步数据流框架
代码示例:

   runBlocking {
            flow {
                emit(1)
                emit(2)
                emit(3)
                emit(4)
                emit(5)
            }.filter { it > 2 }
                .map { it * 2 }
                .take(2)
                .collect {
                    Log.d("FLOW", it.toString())
                }
        }

flow是冷流,只有订阅者订阅时,才开始执行发射数据流的代码
即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据,从上述例子看,当调用了collect后,才会执行flow语句块里面的代码,并且flow每次重新订阅收集都会将所有事件重新发送一次

但是在我们的开发场景中,一般是先触发某个事件(比如请求数据之后)才会去刷新UI,显然flow不适用于这种场景,因为flow只有在下游开始消费时才会触发生产数据

因此引入一个新的概念,StateFlow:

StateFlow与Flow的区别是StateFlow是热流,即无论下游是否有消费行为,上游都会自己产生数据。
代码示例:

在ViewModel创建StateFlow,发送UI状态,关于UI状态下面会讲,这里主要了解StateFlow的用法

//创建flow
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
    get() = _state

//发送UI状态
state.value = ViewState.Loading

在Activity中接收:

    mViewModel.state.collect{
        when(it) {
            is ViewState.Default -> {

            }
            is ViewState.Loading -> {
                //展示加载中页面
                tvLoading.visibility = View.VISIBLE
            }
            is ViewState.BannerMsg -> {
                //加载完成,绑定数据
                tvLoading.visibility = View.GONE
                tvError.visibility = View.GONE
                mAdapter.setData(it.data)
            }
            is ViewState.Error -> {
                //加载失败,展示错误页面
                tvError.visibility = View.VISIBLE
            }
        }
}

看起来这个StateFlow用法和MVVM中的LiveData类似,那它们有什么区别呢?

区别1:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。

区别2:当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,停止发送数据, 而从 StateFlow 收集数据的操作并不会自动停止。如需要实现LiveData相同的行为,可以在 Lifecycle.repeatOnLifecycle 块中去观察数据流。

MVI框架构建:

Intent介绍:

上面提到,intent指的是用户意图,在代码层面上来说他其实就是个枚举类,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用写else

sealed class UserIntent {
    object GetBanners: UserIntent() //定义了一个用户获取Banner的意图
}

处理Intent

这里需要了解一下:Chnnel

channel主要用于协程之间的通讯,使用send和receive往通道里写入或者读取数据,2个方法为非阻塞挂起函数,channel是热流,不管有没有订阅者都会发送。
我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
根据上面的例子,用Channel把UserIntent处理一下:
在View Model定义并观察用户意图:

class UserViewModel : ViewModel() {
    val userIntent = Channel<UserIntent>()  //定义用户意图
    
    init {
        observeUserIntent()
    }

    private fun observeUserIntent() {  //观察用户意图
        viewModelScope.launch {
            userIntent.consumeAsFlow().collect{
                when(it) {
                    is UserIntent.GetBanners -> {
                        loadBanner()
                    }
                }
            }
        }
    }
    

Activity中发送用户意图:

class MainActivity : AppCompatActivity() {
    private val mViewModel by lazy {
        ViewModelProvider(this)[UserViewModel::class.java]
    }
    private val mAdapter by lazy { BannerAdapter() }
    private lateinit var rvData: RecyclerView
    private lateinit var tvLoading: TextView
    private lateinit var tvError: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
        loadData()
    }

    private fun initView() {
        rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = mAdapter
        }
        tvLoading = findViewById(R.id.loading)
        tvError = findViewById(R.id.load_error)
    }

    private fun loadData() {  //将用户意图传给view Model
        lifecycleScope.launch {
            mViewModel.userIntent.send(UserIntent.GetBanners)
        }
    }

看完上面的代码,MVI中的View到Model之间的数据流向就已经清晰了,
接下来就是Model向View层传递数据的过程

State介绍:

State是UI状态,MVI的一个特点就是数据状态统一管理,state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
定义一个State类:

sealed class ViewState {
    object Default: ViewState() //页面默认状态
    object Loading : ViewState() //页面加载
    data class BannerMsg(val data: List<Banner>?): ViewState() //页面加载完成
    data class Error(val error: String?): ViewState() //页面加载错误
}

处理State

在ViewModel中观测到用户意图,根据用户意图去做相关操作,然后将UI State反馈给用户


class UserViewModel : ViewModel() {
    val userIntent = Channel<UserIntent>()
    private val _state = MutableStateFlow<ViewState>(ViewState.Default)
    val state: StateFlow<EnglishState>
        get() = _state

    init {
        observeUserIntent() 
    }

    private fun observeUserIntent() {
        viewModelScope.launch { //观测用户意图
            userIntent.consumeAsFlow().collect{
                when(it) {
                    is UserIntent.GetBanners -> { 
                        loadBanner()  
                    }
                }
            }
        }
    }

    private fun loadBanner() {
        viewModelScope.launch {
            state.value = ViewState.Loading //加载中状态 反馈给View层
            val banners = CloudService.cloudApi.getBanner() //获取数据
            banners.data?.let {
                state.value = ViewState.BannerMsg(it)  //加载成功状态,数据反馈给View
                return@launch
            }
            state.value = ViewState.Error(banners.errorMsg) //加载错误状态反馈给View
        }
    }
}

Activity中观察页面状态:

class MainActivity : AppCompatActivity() {
   private val mViewModel by lazy {
       ViewModelProvider(this)[UserViewModel::class.java]
   }
   private val mAdapter by lazy { BannerAdapter() }
   private lateinit var rvData: RecyclerView
   private lateinit var tvLoading: TextView
   private lateinit var tvError: TextView
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       initView()
       observeViewModel()
       loadData()
   }

   private fun initView() {
       rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
           layoutManager = LinearLayoutManager(this@MainActivity)
           adapter = mAdapter
       }
       tvLoading = findViewById(R.id.loading)
       tvError = findViewById(R.id.load_error)
   }

   private fun loadData() {
       lifecycleScope.launch { 
       //发送用户意图
           mViewModel.userIntent.send(UserIntent.GetBanners)
       }
   }

   private fun observeViewModel() {
       lifecycleScope.launch {
           mViewModel.state.collect{  //观测UI状态,根据不同的状态刷新Ui
               when(it) {
                   is ViewState.Default -> {
                   //初始值不做任何操作
                   }
                   is ViewState.Loading -> {
                       //展示加载中页面
                       tvLoading.visibility = View.VISIBLE
                   }
                   is ViewState.BannerMsg -> {
                       //加载完成,绑定数据
                       tvLoading.visibility = View.GONE
                       tvError.visibility = View.GONE
                       mAdapter.setData(it.data)
                   }
                   is ViewState.Error -> {
                       //加载失败,展示错误页面
                       tvError.visibility = View.VISIBLE
                   }
               }
           }
       }
   }
}

MVI架构主要代码介绍完毕。

MVI总结:

MVI强调数据的单向流动,主要分为几步:

  • 用户操作以Intent的形式通知Model.

  • Model基于Intent更新State

  • View接收到State变化刷新UI

数据永远在一个环形结构中单向流动,不能反向流动。

MVI优缺点

优点:

  • MVI的核心思想是 view-intent-viewmodel-state-view 单向数据流,MVVM核心思想是 view-viewmodel-view 双向数据流
    • 代码分层更清晰,viewmodel 无需关心view如何触发和更新,只需要维护intentstate即可
    • IntentState的引入解决了ViewModelModel的界限模糊问题

缺点:

  • 单向流和双向流,并非 好和不好 的选择,而是 适合和不适合 的选择,业务逻辑较为简单的界面,它不需要mvi、mvvm、mvp、mvc,只一个activity或者fragment + layout 即可,一味的套用架构,反而适得其反!

  • 逻辑、数据、UI 较为复杂时,intentstate将会变得臃肿

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505246.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node框架 【Koa】之 【静态资源管理、模板引擎、连接数据库】

文章目录 &#x1f31f;前言&#x1f31f;静态资源托管&#x1f31f;安装&#x1f31f;使用 &#x1f31f;Koa视图&#x1f31f;EJS模板引擎使用&#x1f31f;安装&#x1f31f;配置&#x1f31f;使用&#x1f31f;模板渲染方法&#x1f31f;使用案例 &#x1f31f;数据库&…

OBS直播时编码器、码率控制器、分辨率帧率是什么以及如何向第三方推流

内容摘要&#xff1a;OBS直播时编码器、码率控制器、分辨率、帧率到底是什么&#xff0c;以及OBS向第三方直播平推流时&#xff0c;要注意什么。 图&#xff1a;OBS直播时输出界面参数设定 OBS编码器 1. 软编&#xff1a;x264 使用CPU进行编码&#xff0c;占用CPU资源多&…

m3u8文件

#EXTM3U&#xff1a;m3u文件头&#xff0c;必须放在第一行&#xff0c;起标示作用&#xff1b; #EXT-X-VERSION&#xff1a;播放列表文件的兼容版本。若不存在此标记&#xff0c;则默认为协议的第一个版本&#xff1b; #EXT-X-MEDIA-SEQUENCE&#xff1a; 播放列表中的每个媒…

第1章计算机系统漫游之 “操作系统管理硬件“

7、操作系统管理硬件 回到 hello 程序的例子。当 shell 加载和运行 hello 程序时&#xff0c;当 hello 程序输出自己的消息时&#xff0c;程序没有直接访问键盘、显示器、磁盘或主存储器。取而代之的是&#xff0c;它们依靠操作系统提供的服务。 可以把操作系统看成是应用程序…

王琤:当数据治理遇上ChatGPT

以ChatGPT为代表的人工智能等技术正在“狂飙”&#xff0c;为全球带来一场翻天覆地的变革。4月27日在2023数据治理新实践峰会上&#xff0c;Datablau数语科技创始人&CEO王琤先生以《数据治理新实践与人工智能》为主题进行了分享&#xff0c;与参会同仁共同探索当数据治理遇…

APP开发的上线流程

APP的使用已经非常普及&#xff0c;对于企业来说通过APP可以加强和客户的沟通&#xff0c;展现最新的产品和服务。随着APP应用商店对用户隐私的重视&#xff0c;APP的上线规则比以前更加复杂&#xff0c;甚至出现APP需要反复修改数十次才能上架的问题&#xff0c;今天和大家分享…

移远通信参加2023中国移动5G发展大会 ,共探5G创新未来

5月6日&#xff0c;以“聚力5G创新&#xff0c;共铸百业绽放”为主题的中国移动5G发展大会在郑州国际会展中心召开。作为5G赋能千行百业的重要贡献者&#xff0c;移远通信应邀参加大会&#xff0c;并深度参与了大会的多个环节。 会上&#xff0c;中国移动系列创新成果发布&…

【Linux】Linux安装tomcat(图文解说详细版)

文章目录 1、安装前置条件2、下载所需压缩包&#xff0c;上传到服务器3、对资源进行解压4、给防火墙添加访问端口&#xff08;默认8080&#xff0c;在它的/conf/server.xml文件里面查看&#xff09;5、然后切换到bin目录下&#xff0c;启动&#xff08;成功效果如图&#xff09…

批量任务导致页面卡死解决方案

需求背景 需要基于高德地图展示海量点位&#xff08;大概几万个&#xff09;&#xff0c;点位样式要自定义&#xff08;创建DOM&#xff09;&#xff0c;虽然使用了聚合点&#xff0c;但初始化时仍需要将几万个点位的DOM结构都创建出来。 这里补充一句&#xff0c;高德地图在2.…

为什么剑桥出身的“AI教父”辛顿会担心?

剑桥很有意思&#xff01;在那儿呆过的人常常有这样一种感觉&#xff1a;剑&#xff0c;很锋利&#xff01;桥&#xff0c;很温柔&#xff01;剑桥的科技自不必说&#xff0c;牛顿、达尔文、麦克斯韦、爱丁顿......&#xff0c;剑桥的人文却也不让科技&#xff0c;拜伦、培根、…

使用asp.net core web api创建web后台,并连接和使用Sql Server数据库

前言&#xff1a;因为要写一个安卓端app&#xff0c;实现从服务器中获取电影数据&#xff0c;所以需要搭建服务端代码&#xff0c;之前学过C#&#xff0c;所以想用C#实现服务器段代码用于测试&#xff0c;本文使用C#语言&#xff0c;使用asp.net core web api组件搭建服务器端&…

【软考备战·希赛网每日一练】2023年5月9日

文章目录 一、今日成绩二、错题总结第一题三、知识查缺 题目及解析来源&#xff1a;2023年05月09日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 有损、无损连接判断&#xff1a; (A1,A2)∩(A1,A3)A1 (A1,A2)-(A1,A3)A2 (A1,A3)-(A1,A2)A3 所以A1-&…

车载5G放量增长,哪些厂商抢跑

前装标配19.88万辆&#xff0c;同比上年同期增长724.89%&#xff0c;这是一季度中国市场乘用车5G搭载上车交付的成绩。高工智能汽车研究院监测数据显示&#xff0c;2022年全年5G交付搭载41.74万辆&#xff08;不含选装&#xff09;&#xff0c;前装搭载率为2.09%。 这意味着&a…

【Linux】GDB多进程调试

目录 GDB多进程调试 GDB多进程调试 演示父子进程如何进行gdb调试会用到hello.c文件 hello.c文件内容如下&#xff1a; #include <stdio.h> #include <unistd.h>int main() {printf("begin\n");if(fork() > 0) {printf("我是父进程&#xff1…

Baumer工业相机堡盟工业相机如何使用BGAPI SDK进行两个万兆网相机的同步采集

Baumer工业相机堡盟工业相机如何使用BGAPI SDK进行两个万兆网相机的同步采集 Baumer工业相机Baumer工业相机图像数据转为Bitmap的技术背景Baumer同步异常 &#xff1a;客户使用两个Baumer万兆网相机进行同步采集发现FrameID相同&#xff0c;但是图像不同步细节原因解决办法 Bau…

Windows下python中的pip换源

在Windows中更换pip数据源方法&#xff0c;提高Python相关包安装效率 1.在windows环境下&#xff0c;打开我的电脑&#xff0c;在"地址栏"输杰沫入: %APPDATA% 后回车 2.在打开的文件夹中新建 pip 文件夹&#xff08;打开的地址为下图所示&#xff09; 3.进入pip文…

在 Python 中将泊松分布拟合到不同的数据集

文章目录 在 Python 中将泊松分布拟合到不同的数据集在 Python 中拟合泊松分布的分箱最小二乘法程序的导入函数为泊松分布创建一个虚拟数据集并使用该数据集绘制直方图使用曲线拟合将曲线拟合到直方图 使用负二项式拟合过度分散的数据集上的泊松分布创建数据集使用数据集绘制直…

JAVA算法(一)查找算法

一、基本查找 / 顺序查找 核心&#xff1a;从0索引开始挨个往后查找 private static boolean basicSearch(int[] arr, int number) {for (int i 0; i < arr.length; i) {if (arr[i] number) {return true;}}return false;}二、二分查找 / 折半查找 前提&#xff1a;数组…

(四) 打造更加智能的即时通信系统——实现主界面消息和联系人切换效果

文章目录 一、引言二、界面设计的基本要求2.1 界面美观简洁2.2 功能合理布局 三、界面布局和控件设计四、效果展示五、关键代码六、个人经验分享6.1 即时通信系统开发中的经验和总结6.2 遇到的问题和解决方案6.3优化即时通信系统 七、总结 一、引言 当今社会&#xff0c;人们对…

解决 scalac: bad option: ‘-make:transitive‘

scalac: bad option: ‘-make:transitive’ 打开项目所在位置并进入 .idea 修改scala_compiler.xml文件 删除掉参数行包含-make:transitive 保存后 重新运行代码