当前位置: 首页 > news >正文

MVI 架构更佳实践:支持 LiveData 属性监听

前言

MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变+不可变)的问题,使用ViewState对State集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态

通过集中管理ViewState,只需对外暴露一个LiveData,解决了MVVM模式下LiveData膨胀的问题

但页面的所有状态都通过一个LiveData来管理,也带来了一个严重的问题,即页面不支持局部刷新

虽说如果是RecyclerView可以通过DifferUtil来解决,但毕竟不是所有页面都是通过RecyclerView写的,支持DifferUtil也有一定的开发成本

因此直接使用MVI架构会带来一定的性能损耗,相信这是很多人不愿意用MVI架构的原因之一

本文主要介绍如何通过监听LiveData的属性,来实现MVI架构下的局部刷新

Mavericks框架介绍

Mavericks框架是Airbnb开源的一个MVI框架,Mavericks基于Android Jetpack与Kotlin Coroutines,主要目标是使页面开发更高效,更容易,更有趣,目前已经在Airbnb的数百个页面上使用

下面我们来看下Mavericks是怎么使用的

// 1. 包含页面所有状态的data class
data class CounterState(val count: Int = 0) : MavericksState// 2.负责处理业务逻辑的ViewModel,易于单元测试
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {// 通过setState更新页面状态fun incrementCount() = setState { copy(count = count + 1) }
}// 3. View层,必须实现MavericksView接口   
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {private val viewModel: CounterViewModel by fragmentViewModel()override fun onViewCreated(view: View, savedInstanceState: Bundle?) {counterText.setOnClickListener {viewModel.incrementCount()}}//4. 页面刷新回调,每当状态刷新时会回调这里override fun invalidate() = withState(viewModel) { state ->counterText.text = "Count: ${state.count}"}
}

如上所示,看上去也很简单,主要包括几个模块

  1. 包括页面所有状态的Model层,其中的状态全都是不可变的,并且有默认值

  1. 负责处理业务逻辑的ViewModel,在其中通过setState来更新页面状态

  1. View层,必须实现MavericksView接口,每当状态刷新时都会回调invalidate函数,在这里渲染UI

可以看出,Mavericks中View层与Model层的交互,也并没有包装成Action,而是直接暴露的方法

上篇文章也的确有很多同学说使用Action交互比较麻烦,看起来Action这层的确可要可不要,Airbnb也没有使用,主要看个人开发习惯吧

支持局部刷新

上面介绍了Mavericks的简单使用,下面我们来看下Mavericks是怎么实现局部刷新的

data class UserState(val score: Int = 0,val previousHighScore: Int = 150,val livesLeft: Int = 99,
) : MavericksState {val pointsUntilHighScore = (previousHighScore - score).coerceAtLeast(0)val isHighScore = score >= previousHighScore
}class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {override fun onViewCreated(view: View, savedInstanceState: Bundle?) {//直接监听State的属性,并且支持设置监听模式  viewModel.onEach(UserState::pointsUntilHighScore,deliveryMode = uniqueOnly()) {//..}viewModel.onEach(UserState::score) { //...}    }
}
  1. 如上所示,Mavericks可以只监听State的其中一个属性来实现局部刷新,只有当这个属性发生变化时才触发回调

  1. onEach也可以设置监听模式,主要是为了防止数据倒灌,例如Toast这些只需要弹一次,页面重建时不应该恢复的状态,就适合使用uniqueOnly的监听模式

Mavericks实现属性监听的原理也很简单,我们一起来看下源码

fun <VM : MavericksViewModel<S>, S : MavericksState, A> VM._internal1(owner: LifecycleOwner?,prop1: KProperty1<S, A>,deliveryMode: DeliveryMode = RedeliverOnStart,action: suspend (A) -> Unit
) = stateFlow// 通过对象取出属性的值.map { MavericksTuple1(prop1.get(it)) }// 值发生变化了才会触发回调  .distinctUntilChanged().resolveSubscription(owner, deliveryMode.appendPropertiesToId(prop1)) { (a) ->action(a)}
  1. 主要是通过map将State转化为它的属性值

  1. 通过distinctUntilChanged方法开启防抖,相同的值不会回调,只有值修改了才会回调

  1. 需要注意的是因为使用了KProperty1,因此State的承载数据类必须避免混淆

如上,就是Mavericks的基本介绍,想了解更多的同学可参考:github.com/airbnb/mave…

LiveData实现属性监听

上面介绍了Mavericks是怎么实现局部刷新的,但直接使用它主要有两个问题

  1. 接入起来略微有点麻烦,例如Fragment必须实现MavericksView,有一定接入成本

  1. Mavericks的局部刷新是通过Flow实现的,但相信大多数人用的还是LiveData,有一定学习成本

下面我们就来看下LiveData怎么实现属性监听

//监听一个属性
fun <T, A> LiveData<T>.observeState(lifecycleOwner: LifecycleOwner,prop1: KProperty1<T, A>,action: (A) -> Unit
) {this.map {StateTuple1(prop1.get(it))}.distinctUntilChanged().observe(lifecycleOwner) { (a) ->action.invoke(a)}
}//监听两个属性
fun <T, A, B> LiveData<T>.observeState(lifecycleOwner: LifecycleOwner,prop1: KProperty1<T, A>,prop2: KProperty1<T, B>,action: (A, B) -> Unit
) {this.map {StateTuple2(prop1.get(it), prop2.get(it))}.distinctUntilChanged().observe(lifecycleOwner) { (a, b) ->action.invoke(a, b)}
}internal data class StateTuple1<A>(val a: A)
internal data class StateTuple2<A, B>(val a: A, val b: B)//更新State
fun <T> MutableLiveData<T>.setState(reducer: T.() -> T) {this.value = this.value?.reducer()
}
  1. 如上所示,主要是添加一个扩展方法,也是通过distinctUntilChanged来实现防抖

  1. 如果需要监听多个属性,例如两个属性有其中一个变化了就触发刷新,也支持传入两个属性

  1. 需要注意的是LiveData默认是不防抖的,这样改造后就是防抖的了,所以传入相同的值是不会回调的

  1. 同时需要注意下承载State的数据类需要防混淆

简单使用

上面介绍了LiveData如何实现属性监听,下面看下简单的使用

//页面状态,需要避免混淆
data class MainViewState(val fetchStatus: FetchStatus = FetchStatus.NotFetched,val newsList: List<NewsItem> = emptyList()
)//ViewModel
class MainViewModel : ViewModel() {private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData(MainViewState())//只需要暴露一个LiveData,包括页面所有状态val viewStates = _viewStates.asLiveData()private fun fetchNews() {//更新页面状态_viewStates.setState {copy(fetchStatus = FetchStatus.Fetching)}viewModelScope.launch {when (val result = repository.getMockApiResponse()) {//...is PageState.Success -> {_viewStates.setState {copy(fetchStatus = FetchStatus.Fetched, newsList = result.data)}}}}}}//View层
class MainActivity : AppCompatActivity() {private fun initViewModel() {viewModel.viewStates.run {//监听newsListobserveState(this@MainActivity, MainViewState::newsList) {newsRvAdapter.submitList(it)}//监听网络状态observeState(this@MainActivity, MainViewState::fetchStatus) {//..}}}
}

如上所示,其实使用起来也很简单方便

  1. ViewModel只需对外暴露一个ViewState,避免了定义多个可变不可变LiveData的问题

  1. View层支持监听LiveData的一个属性或多个属性,支持局部刷新

总结

本文主要介绍了MVI架构下如何实现局部刷新,并重点介绍了Mavericks的基本使用与原理,并在其基础上使用LiveData实现了属性监听与局部刷新

通过以上方式,解决了MVI架构的性能问题,实现了MVI架构的更佳实践

如果你的ViewModel中定义了多个可变与不可变的LiveData,就算你不使用MVI架构,支持监听LiveData属性相信也可以帮助你精简一定的代码

如果本文对你有所帮助,欢迎点赞关注Star~

源码地址:

https://github.com/shenzhen2017/android-architecture

相关文章:

MVI 架构更佳实践:支持 LiveData 属性监听

前言MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变不可变)的问题,使用ViewState对State集中管理&#xff0c;只需要订阅一个 ViewState 便可获取页面的所有状态通过集中管理ViewState&#xff0c;只需对外暴露一个LiveData&#xff0c;解决了MVVM模式下LiveData膨胀…...

LeetCode438 找到字符串中所有字母异位词 带输入和输出

题目&#xff1a; 给定两个字符串 s 和 p&#xff0c;找到 s 中所有 p 的 异位词 的子串&#xff0c;返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成的字符串&#xff08;包括相同的字符串&#xff09;。 示例 1: 输入: s “cbaebabacd”, …...

ACSC 2023 比赛复现

Admin Dashboard 在 index.php 中可以看到需要访问者是 admin 权限&#xff0c;才可以看到 flag。 report.php 中可以让 admin bot 访问我们输入的 url&#xff0c;那么也就是说可以访问 addadmin.php 添加用户。 在 addadmin.php 中可以添加 admin 用户&#xff0c;但是需…...

【Linux驱动开发100问】什么是模块?如何编写和使用模块?

&#x1f947;今日学习目标&#xff1a;什么是Linux内核&#xff1f; &#x1f935;‍♂️ 创作者&#xff1a;JamesBin ⏰预计时间&#xff1a;10分钟 &#x1f389;个人主页&#xff1a;嵌入式悦翔园个人主页 &#x1f341;专栏介绍&#xff1a;Linux驱动开发100问 什么是模块…...

Android 9.0 Recent列表不显示某个app

1.概述 在9.0的系统产品rom定制化开发中,在一些产品定制化需求中,也是有很多重要的功能实现的,比如在某些app的开发中 由于不想被杀掉,所以就不想出现在recent的列表中,因此就需要从recent的列表中,去掉这个app的显示,然后这里有 两种方法实现这个功能,一种是在app中就…...

深度学习之卷积神经网络学习笔记一

1. 引言深度学习是一系列算法的统称&#xff0c;包括卷积神经网络&#xff08;CNN&#xff09;&#xff0c;循环神经网络&#xff08;RNN&#xff09;&#xff0c;自编码器&#xff08;AE&#xff09;&#xff0c;深度置信网络&#xff08;DBN&#xff09;&#xff0c;生成对抗…...

黑盒测试的常用方法

这里我们先设置一个示例,后面的文章中会根据示例来进行讲解 假设有一个程序是判断一个整形数字是否属于1-100 目录 1.等价类法 2.边界值法 3.判定表法 4.场景设计法 5.错误猜测法 6.正交法 1.等价类法 概念:系统性的确定要输入的测试条件的方法可以看出概念非常抽象,那…...

操作系统笔记-第一章

文章目录操作系统概述1. 操作系统的概念1.1 操作系统的地位1.2 操作系统的作用1.3 操作系统的定义2. 操作系统的历史2.1 操作系统的产生2.1.1 手动操作阶段&#xff08;20世纪40年代&#xff09;2.1.2 批处理阶段&#xff08;20世纪50年代&#xff09;2.1.3 执行系统阶段&#…...

daillist

daillist #重要说明&#xff1a; #[1]任意两个配置参数之间必须以空格隔开&#xff0c;否则&#xff0c;拨号脚本无法识别。 #[2]Info格式说明:厂商名简称_制式_频段 #VID #PID #PORT_M #PORT_A #PORT_G #script_*99# #script_#777 #Info 05c6 9025 /dev/ttyUSB1 /dev/ttyUSB2 …...

vue中render函数的作用和参数(vue2中render函数用法)

render 函数是 Vue2.x 新增的一个函数、主要用来提升节点的性能&#xff0c;它是基于 JavaScript 计算。使用 Render 函数将 Template 里面的节点解析成虚拟的 Dom 。Vue 推荐在绝大多数情况下使用模板来创建 HTML。然而在一些场景中&#xff0c;需要 JavaScript 的完全编程能力…...

基于Istio的高级流量管理二(Envoy流量劫持、Istio架构、高级流量管理)

文章目录一、Envoy流量劫持机制&#xff08;Iptables规则流转&#xff09;1、流量出向劫持流程&#xff08;1&#xff09;envoy怎样劫持入向流量&#xff1f;&#xff08;2&#xff09;Envoy劫持到流量之后&#xff0c;干什么&#xff1f;&#xff08;查询目的地&#xff09;&a…...

Sharding-Springboot-mybatis-plus整合(三)-inline策略

Sharding-Springboot-mybatis-plus整合&#xff08;三&#xff09; 1.简介 本节目标&#xff0c;使用SpringBoot整合Sharding和Mybatis-Plus验证上节分片策略 从配置文件上看策略包括&#xff08; inline、standard、complex、hint&#xff09; 环境搭建以inline策略演示 …...

编码的基本概念

本专栏包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;information-theory】&#xff0c;需要的朋友们自取。或者公众号【AIShareLab】回复 信息论 也可获取。 文章目录信源编码分类前缀…...

函数指针与指针函数的区别

目录&#xff1a;一、函数指针1 函数类型2 函数指针(指向函数的指针)3 函数指针数组二.函数指针和指针函数比较1 定义不同2 写法不同3.用法不同三.函数指针做函数参数(回调函数)1 利用回调函数实现打印任意类型数据2 提供能够打印任意类型数组函数3 利用回调函数 提供查找功能四…...

死锁的四个必要条件以及如何避免死锁

死锁的四个必要条件以及如何避免死锁 一.什么是死锁&#xff1f;二.死锁的四个必要条件 1.互斥条件&#xff1a;2.请求与保持条件&#xff1a;3.不剥夺条件:4.循环等待条件: 三.如何避免死锁 1.破坏请求保持条件2.破坏不剥夺条件3.破坏循环等待条件 死锁的四个必要条件以及如…...

浏览器多线程到事件循环机制

浏览器与js运行机制 进程与线程 进程 进程是CPU分配资源的最小单位&#xff0c;它是一个可以自己独立运行且拥有自己资源空间的任务程序&#xff1b;包括程序以及程序所使用的内存及系统资源 线程 线程是CPU调度的最小单位&#xff0c;它就是程序中的一个执行流&#xff1…...

Lambda表达式的本质

一直想写一篇文章&#xff0c;来总结lambda表达式&#xff0c;但是之前感觉总结的不是特别到位&#xff0c;现在看了几篇文章和视频后&#xff0c;感觉对lambda表达式有了比较深刻的认识&#xff0c;现在进行记录总结如下&#xff1a; lambda表达式又叫做匿名函数&#xff0c;…...

类的加载过程(生命周期)

类的加载过程(生命周期) 一、装载&#xff1a;通过一个类的全限定名获取定义此类的二进制字节流将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象&#xff08;将字节码加载到内存中&#xff09;&#xff0c;作为…...

2023最新谷粒商城笔记之MQ消息队列篇(全文总共13万字,超详细)

MQ消息队列 其实队列JDK中本身就有&#xff0c;不过这种队列也只能单体服务可能会使用&#xff0c;一旦项目使用的分布式架构&#xff0c;那么一定还是需要用到一个消息中间件的。我们引入消息队列的原因就是对我们的页面相应速度再优化&#xff0c;让用户的体验更好&#xff…...

多变量线性回归模型

多变量线性回归模型 模型参数为n1维向量&#xff0c;此时模型公式为 hθ(x)θ0x0θ1x1θ2x2...θnxnh_{\theta}(x)\theta_{0}x_{0}\theta_{1}x_{1}\theta_{2}x_{2}...\theta_{n}x_{n} hθ​(x)θ0​x0​θ1​x1​θ2​x2​...θn​xn​ 可以简化为 hθ(x)θTXh_{\theta}(x)\th…...

基础测试工具使用经验

背景 vtune&#xff0c;perf, nsight system等基础测试工具&#xff0c;都是用过的&#xff0c;但是没有记录&#xff0c;都逐渐忘了。所以写这篇博客总结记录一下&#xff0c;只要以后发现新的用法&#xff0c;就记得来编辑补充一下 perf 比较基础的用法&#xff1a; 先改这…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲

文章目录 前言第一部分&#xff1a;体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分&#xff1a;体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

鸿蒙(HarmonyOS5)实现跳一跳小游戏

下面我将介绍如何使用鸿蒙的ArkUI框架&#xff0c;实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...

2025-05-08-deepseek本地化部署

title: 2025-05-08-deepseek 本地化部署 tags: 深度学习 程序开发 2025-05-08-deepseek 本地化部署 参考博客 本地部署 DeepSeek&#xff1a;小白也能轻松搞定&#xff01; 如何给本地部署的 DeepSeek 投喂数据&#xff0c;让他更懂你 [实验目的]&#xff1a;理解系统架构与原…...

计算机系统结构复习-名词解释2

1.定向&#xff1a;在某条指令产生计算结果之前&#xff0c;其他指令并不真正立即需要该计算结果&#xff0c;如果能够将该计算结果从其产生的地方直接送到其他指令中需要它的地方&#xff0c;那么就可以避免停顿。 2.多级存储层次&#xff1a;由若干个采用不同实现技术的存储…...

用js实现常见排序算法

以下是几种常见排序算法的 JS实现&#xff0c;包括选择排序、冒泡排序、插入排序、快速排序和归并排序&#xff0c;以及每种算法的特点和复杂度分析 1. 选择排序&#xff08;Selection Sort&#xff09; 核心思想&#xff1a;每次从未排序部分选择最小元素&#xff0c;与未排…...

MLP实战二:MLP 实现图像数字多分类

任务 实战&#xff08;二&#xff09;&#xff1a;MLP 实现图像多分类 基于 mnist 数据集&#xff0c;建立 mlp 模型&#xff0c;实现 0-9 数字的十分类 task: 1、实现 mnist 数据载入&#xff0c;可视化图形数字&#xff1b; 2、完成数据预处理&#xff1a;图像数据维度转换与…...

JUC并发编程(二)Monitor/自旋/轻量级/锁膨胀/wait/notify/锁消除

目录 一 基础 1 概念 2 卖票问题 3 转账问题 二 锁机制与优化策略 0 Monitor 1 轻量级锁 2 锁膨胀 3 自旋 4 偏向锁 5 锁消除 6 wait /notify 7 sleep与wait的对比 8 join原理 一 基础 1 概念 临界区 一段代码块内如果存在对共享资源的多线程读写操作&#xf…...