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}"}
}
如上所示,看上去也很简单,主要包括几个模块
包括页面所有状态的Model层,其中的状态全都是不可变的,并且有默认值
负责处理业务逻辑的ViewModel,在其中通过setState来更新页面状态
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) { //...} }
}
如上所示,Mavericks可以只监听State的其中一个属性来实现局部刷新,只有当这个属性发生变化时才触发回调
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)}
主要是通过map将State转化为它的属性值
通过distinctUntilChanged方法开启防抖,相同的值不会回调,只有值修改了才会回调
需要注意的是因为使用了KProperty1,因此State的承载数据类必须避免混淆
如上,就是Mavericks的基本介绍,想了解更多的同学可参考:github.com/airbnb/mave…
LiveData实现属性监听
上面介绍了Mavericks是怎么实现局部刷新的,但直接使用它主要有两个问题
接入起来略微有点麻烦,例如Fragment必须实现MavericksView,有一定接入成本
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()
}
如上所示,主要是添加一个扩展方法,也是通过distinctUntilChanged来实现防抖
如果需要监听多个属性,例如两个属性有其中一个变化了就触发刷新,也支持传入两个属性
需要注意的是LiveData默认是不防抖的,这样改造后就是防抖的了,所以传入相同的值是不会回调的
同时需要注意下承载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) {//..}}}
}
如上所示,其实使用起来也很简单方便
ViewModel只需对外暴露一个ViewState,避免了定义多个可变不可变LiveData的问题
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集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态通过集中管理ViewState,只需对外暴露一个LiveData,解决了MVVM模式下LiveData膨胀…...
LeetCode438 找到字符串中所有字母异位词 带输入和输出
题目: 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。 示例 1: 输入: s “cbaebabacd”, …...
ACSC 2023 比赛复现
Admin Dashboard 在 index.php 中可以看到需要访问者是 admin 权限,才可以看到 flag。 report.php 中可以让 admin bot 访问我们输入的 url,那么也就是说可以访问 addadmin.php 添加用户。 在 addadmin.php 中可以添加 admin 用户,但是需…...
【Linux驱动开发100问】什么是模块?如何编写和使用模块?
🥇今日学习目标:什么是Linux内核? 🤵♂️ 创作者:JamesBin ⏰预计时间:10分钟 🎉个人主页:嵌入式悦翔园个人主页 🍁专栏介绍:Linux驱动开发100问 什么是模块…...
Android 9.0 Recent列表不显示某个app
1.概述 在9.0的系统产品rom定制化开发中,在一些产品定制化需求中,也是有很多重要的功能实现的,比如在某些app的开发中 由于不想被杀掉,所以就不想出现在recent的列表中,因此就需要从recent的列表中,去掉这个app的显示,然后这里有 两种方法实现这个功能,一种是在app中就…...
深度学习之卷积神经网络学习笔记一
1. 引言深度学习是一系列算法的统称,包括卷积神经网络(CNN),循环神经网络(RNN),自编码器(AE),深度置信网络(DBN),生成对抗…...
黑盒测试的常用方法
这里我们先设置一个示例,后面的文章中会根据示例来进行讲解 假设有一个程序是判断一个整形数字是否属于1-100 目录 1.等价类法 2.边界值法 3.判定表法 4.场景设计法 5.错误猜测法 6.正交法 1.等价类法 概念:系统性的确定要输入的测试条件的方法可以看出概念非常抽象,那…...
操作系统笔记-第一章
文章目录操作系统概述1. 操作系统的概念1.1 操作系统的地位1.2 操作系统的作用1.3 操作系统的定义2. 操作系统的历史2.1 操作系统的产生2.1.1 手动操作阶段(20世纪40年代)2.1.2 批处理阶段(20世纪50年代)2.1.3 执行系统阶段&#…...
daillist
daillist #重要说明: #[1]任意两个配置参数之间必须以空格隔开,否则,拨号脚本无法识别。 #[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 新增的一个函数、主要用来提升节点的性能,它是基于 JavaScript 计算。使用 Render 函数将 Template 里面的节点解析成虚拟的 Dom 。Vue 推荐在绝大多数情况下使用模板来创建 HTML。然而在一些场景中,需要 JavaScript 的完全编程能力…...
基于Istio的高级流量管理二(Envoy流量劫持、Istio架构、高级流量管理)
文章目录一、Envoy流量劫持机制(Iptables规则流转)1、流量出向劫持流程(1)envoy怎样劫持入向流量?(2)Envoy劫持到流量之后,干什么?(查询目的地)&a…...
Sharding-Springboot-mybatis-plus整合(三)-inline策略
Sharding-Springboot-mybatis-plus整合(三) 1.简介 本节目标,使用SpringBoot整合Sharding和Mybatis-Plus验证上节分片策略 从配置文件上看策略包括( inline、standard、complex、hint) 环境搭建以inline策略演示 …...
编码的基本概念
本专栏包含信息论与编码的核心知识,按知识点组织,可作为教学或学习的参考。markdown版本已归档至【Github仓库:information-theory】,需要的朋友们自取。或者公众号【AIShareLab】回复 信息论 也可获取。 文章目录信源编码分类前缀…...
函数指针与指针函数的区别
目录:一、函数指针1 函数类型2 函数指针(指向函数的指针)3 函数指针数组二.函数指针和指针函数比较1 定义不同2 写法不同3.用法不同三.函数指针做函数参数(回调函数)1 利用回调函数实现打印任意类型数据2 提供能够打印任意类型数组函数3 利用回调函数 提供查找功能四…...
死锁的四个必要条件以及如何避免死锁
死锁的四个必要条件以及如何避免死锁 一.什么是死锁?二.死锁的四个必要条件 1.互斥条件:2.请求与保持条件:3.不剥夺条件:4.循环等待条件: 三.如何避免死锁 1.破坏请求保持条件2.破坏不剥夺条件3.破坏循环等待条件 死锁的四个必要条件以及如…...
浏览器多线程到事件循环机制
浏览器与js运行机制 进程与线程 进程 进程是CPU分配资源的最小单位,它是一个可以自己独立运行且拥有自己资源空间的任务程序;包括程序以及程序所使用的内存及系统资源 线程 线程是CPU调度的最小单位,它就是程序中的一个执行流࿱…...
Lambda表达式的本质
一直想写一篇文章,来总结lambda表达式,但是之前感觉总结的不是特别到位,现在看了几篇文章和视频后,感觉对lambda表达式有了比较深刻的认识,现在进行记录总结如下: lambda表达式又叫做匿名函数,…...
类的加载过程(生命周期)
类的加载过程(生命周期) 一、装载:通过一个类的全限定名获取定义此类的二进制字节流将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象(将字节码加载到内存中),作为…...
2023最新谷粒商城笔记之MQ消息队列篇(全文总共13万字,超详细)
MQ消息队列 其实队列JDK中本身就有,不过这种队列也只能单体服务可能会使用,一旦项目使用的分布式架构,那么一定还是需要用到一个消息中间件的。我们引入消息队列的原因就是对我们的页面相应速度再优化,让用户的体验更好ÿ…...
多变量线性回归模型
多变量线性回归模型 模型参数为n1维向量,此时模型公式为 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)θ0x0θ1x1θ2x2...θnxn 可以简化为 hθ(x)θTXh_{\theta}(x)\th…...
php 基于ICMP协议实现一个ping命令
php 基于ICMP协议实现一个ping命令 网络协议是什么ICMP 协议什么是ICMP?ICMP 的主要功能ICMP 在 IPv4 和 IPv6 的封装Wireshark抓包ICMP 请求包分析PHP构建 ICMP 数据包php中的 pack & unpack 函数字节和字符packunpackICMP计算校验和步骤总结网络协议是什么 网络协议&…...
Java基本数据类型
1.概述 佛说,大千世界,无奇不有。在这个世界里,物种的多样性,遍地开花,同样,在Java的世界里,也有着异曲同工之妙,Java秉承面向对象的特性,必然少不了区分对象的类型&…...
English Learning - L2 语音作业打卡 Day2 2023.2.22 周三
English Learning - L2 语音作业打卡 Day2 2023.2.22 周三💌 发音小贴士:💌 当日目标音发音规则/技巧:🍭 Part 1【热身练习】🍭 Part2【练习内容】🍭【练习感受】🍓元音[ ɑː ]&…...
45. 跳跃游戏 II
题目: 45. 跳跃游戏 II难度中等1974收藏分享切换为英文接收动态反馈给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 num…...
应届生Java面试50题线程篇(含解析)
什么是线程? 答:线程是操作系统能够进行运算调度的最小单位,是程序执行流的最小单元。在Java中,可以通过实现Runnable接口或继承Thread类来创建线程。 创建线程的方式有哪些?各自的优缺点是什么? 继承 Thread 类&…...
【数据库】第七章 数据库设计
第七章数据库设计 数据库设计概述 数据库设计的基本步骤 需求分析概念结构设计逻辑结构设计物理结构设计数据库实施数据库运行和维护 需求分析 收集需求,理解需求 收集各个角色的需求 概念数据库设计 建立概念模型 ,E-R图/IDEF1x图 消除冲突&…...
Burp Suite 常用模块简介
Burp Suite 常用模块分为 目标站点(target)模块 代理(proxy)模块 攻击(Intruder)模块 重放(Repeater) 模块 Target模块是对站点资源的收集,与站点各资源包发出和相应包的记录 Proxy模块是核心模块,可以拦截数据包发送往浏览器,进行修改后再…...
QML Item和Rectangle详解
1.Item和Rectangle Item类型是Qt Quick中所有可视项的基本类型。 Qt Quick中的所有可视项都继承Item。尽管Item对象没有视觉外观,但它定义了视觉项中常见的所有属性,例如x和y位置、宽度和高度、锚定和键处理支持。 Rectangle继承自Item,多…...
常见前端基础面试题(HTML,CSS,JS)(六)
GET 和 POST 的区别 从 http 协议的角度来说,GET 和 POST 它们都只是请求行中的第一个单词,除了语义不同,其实没有本质的区别。 之所以在实际开发中会产生各种区别,主要是因为浏览器的默认行为造成的。 受浏览器的影响…...
深度学习 李沐报错
3.6. softmax回归的从零开始实现 — 动手学深度学习 2.0.0 documentation softmax从0开始实现 函数执行需要加main指定 改成这样 if __name__"__main__":print(evaluate_accuracy(net, test_iter)) 不然会这样出错 RuntimeError: An attempt has been m…...
怎么用织梦模板做网站/如何网络推广新产品
2019独角兽企业重金招聘Python工程师标准>>> wampserver开启php_ldap扩展问题记录 在win7(64位)下使用wampserver,开启php_ldap扩展问题,除了要在php.ini中配置外,还要把php_ldap必须的 libsasl.dll依赖放…...
找个男做那个视频网站好/济南百度推广公司电话
首先是 注册码问题 推荐使用 zoho 邮箱,这个是免费的企业邮箱,重要的是不用填那些乱七八糟的东西,注册很方便 其二是 安装问题 安装时不要选中 安装后启动和初始化 类似的英文 ,后面可能会卡住,看终端你可以发现写着…...
网站开发语言识别/企业网站运营推广
1、先建立一个包,定义数组类型:CREATE OR REPLACE PACKAGE pkg_string is-- Purpose : 字符串处理-- Public type declarationsTYPE StringArray IS VARRAY(2000) OF VARCHAR2(2000);END pkg_string;2、拆分字符串的通用函数,即数组的函数&am…...
邹平网站建设公司/南宁网站推广营销
创建用户 概述:在oracle中要创建一个新的用户使用create user语句,一般是具有dba(数据库管理员)的权限才能使用。 create user 用户名 identified by 密码; (oracle有个毛病,密码必须以字母开头,如果不以字母开头,它…...
巴中城市建设投资有限公司网站/房地产销售技巧和话术
发现问题我使用的Oracle11g,当我敲下如下一段命令后,就让我傻眼了。。alter system set sga_max_size960M scopespfile;shutdown immediatestartup此时的startup报错了,错误为:SQL> startupORA-00844: Parameter not taking ME…...
登录器显的窗口网站怎么做/看b站视频软件下载安装手机
数据结构实验之图论二:图的深度遍历 Description 请定一个无向图,顶点编号从0到n-1,用深度优先搜索(DFS),遍历并输出。遍历时,先遍历节点编号小的。 Input 输入第一行为整数n(0 < n < 100ÿ…...