Compose 中状重组

一、状态变化

1.1 状态变化是什么

根据上篇文章的讲解,在 Compose 我们使用 State 来声明一个状态,当状态发生变化时,则会触发重组。那么状态变化是指什么呢?
下面我们来看一个例子:

@Composable
fun NumList() {
    val num by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    Column {
        Button(onClick = {
            num += (num.last() + 1)
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

这段代码中,我们定义了一个 State ,其包裹的类型是 MutableList, 并且每次点击,我们就给该 mutableList 增加一个元素。运行一下:

我们点击了按钮,界面并没有发生变化,但是,从日志看到,每次点击后,list 中的元素的确增加了一个。

2024-03-18 20:51:41.472 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4]
2024-03-18 20:51:42.411 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4, 5]
2024-03-18 20:51:43.347 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4, 5, 6]

原因是什么呢?其实状态发生变化,实际上指的是 State 包裹的对象,进行 equals 比较,如果不相等,则认为状态变化,否则认为没有发生变化。所以这里就解释得通了,我们虽然在点击按钮后,给 mutableList 增加了元素,但是 mutableList 在进行前后比较时,比较的是其引用,对象的引用并没有发生变化,所以没有发生重组。【这里结论并不准确,下面稳定类型详细解释说】
那为了让其发生重组,我们稍作修改,每次点击按钮时,创建一个新的 list,然后赋值,看看是不是我们所期待的结果。

@Composable
fun NumList() {
    var num by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    Column {
        Button(onClick = {
            val num1 = num.toMutableList()
            num1 += (num1.last() + 1)
            num = num1
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

再次运行程序:

结果符合我们的预期。那对于 List 类型的数据对象,每次状态发生变化,我们创建了一个新对象,这样在进行 equals 比较时,必定不相等,则会触发重组。

1.2 mutableStateListOf 和 mutableStateMapOf

上面的问题,我们虽然接解决了, 但是写法不够优雅,其实 Compose 给我们提供了一个函数 mutableStateListOf 来解决这类问题,我们看看这个函数怎么用,改写上面的例子

@Composable
fun NumList() {
    val num = remember {
        mutableStateListOf(1, 2, 3)
    }
    Column {
        Button(onClick = {
            num += (num.last() + 1)
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

这样就可以满足我们的需求。 mutableStateListOf 返回了一个可感知内部数据变化的 SnapshotStateList<T>, 它的内部的实现为了保证不变性,仍然是拷贝元素,只不过它用了更加高效的实现,比我们单纯用toMutableList要高效得多。
由于 SnapshotStateList 继承了 MutableList 接口,使得 MutableList 中定义的方法,依然可以使用。
同理,对于 Map 类型的对象, Compose 中提供了 mutableStateMapOf 方法,可以更优雅,更高效地进行处理。

思考如下问题:
假如我定义了一个类型:data class Hero(var name: String, var age: Int), 然后使用 mutableStateListOf 定义了状态,其中的元素是自定义的类型 Hero, 当改变 Hero 的属性时, 与该状态相关的 Composable 是否会发生重组?

data class Hero(var name: String, var age: Int)

@Composable
fun HeroInfo() {
    val heroList = remember {
        mutableStateListOf(Hero(name = "安其拉", age = 18), Hero(name = "鲁班", age = 19))
    }

    Column {
        Button(onClick = {
            heroList[0].name = "DaJi"
            heroList[0].age = 22
        }) {
            Text(text = "test click")
        }

        heroList.forEach {
            Text(text = "student, name: ${it.name}, age: ${it.age} ")
        }
    }
}

二、重组的特性

2.1 Composable 重组是智能的

传统 View 体系通过修改 View 的私有属性来改变 UI, Compose 则通过重组刷新 UI。 Compose 的重组非常“智能”,当重组发生时,只有状态发生更新的 Composable 才会参与重组,没有变化的 Composable 会跳过本次重组。

@Composable
fun KingHonour() {
    Column {
        var name by remember {
            mutableStateOf("周瑜")
        }
        Button(onClick = {
            name = "小乔"
        }) {
            Text(text = "改名")
        }
        Text(text = "鲁班")
        Text(text = name)

    }
}

该例子中,点击按钮,改变了 name 的值,触发重组,Button 和 Text(text = "鲁班"),并不依赖该状态,虽然在重组时被调用了,但是在运行时并不会真正的执行。因为其参数没有变化,Compose 编译器会在编译器插入相关的比较代码。只有最后一个 Text 依赖该状态,会参与真正的重组。

2.2 Composable 会以任意顺序执行

@Composable
fun Navi() {
    Box {
        FirstScreen()
        SecondScreen()
        ThirdScreen()
    }
}

在代码中出现多个 Composable 函数时,它们并不一定按照在代码中出现的顺序执行,比如在一个 Box 中,处于前景的 UI 具有较高优先级。所以不要试图通过外部变量与其它 Composable 产生关联。

2.3 Composable 会并发执行

重组中的 Composable 并不一定执行在 UI 线程,它们可以在后台线程中并发执行,这样利于发挥多喝处理器的优势。正因为此,也需要考虑线程安全问题。

2.4 Composable 会反复执行

除了重组会造成 Composable 的再次执行外,在动画等场景中每一帧的变化都可能引起 Composable 的执行。因此 Composable 可能在短时间内多次执行。

2.5 Composable 的执行是“乐观”的

所谓“乐观”是指 Composable 最终会依据最新的状态正确地完成重组。在某些场景下,状态可能会连续变化,可能会导致中间态的重组在执行时被打断,新的重组会插进来,对于被打断的重组,Compose 不会将执行一半的结果反应到视图树上,因为最后一次的状态总归是正确的。

三、重组范围

原则:重组范围最小化。
只有受到了 State 变化影响的代码块,才会参与到重组,不依赖 State 变化的代码则不参与重组。
如何确定重组范围呢?修改上面的例子:

@Composable
fun RecompositionTest() {
    Column {
        Box {
            Log.i("sharpcj", "RecompositionTest - 1")
            Column {
                Log.i("sharpcj", "RecompositionTest - 2")
                var name by remember {
                    mutableStateOf("周瑜")
                }
                Button(onClick = {
                    name = "小乔"
                }) {
                    Log.i("sharpcj", "RecompositionTest - 3")
                    Text(text = "改名")
                }
                Text(text = "鲁班")
                Text(text = name)
            }
        }
        Box {
            Log.i("sharpcj", "RecompositionTest - 4")
        }
        Card {
            Log.i("sharpcj", "RecompositionTest - 5")
        }
    }
}

运行,第一次我们看到,打印了如下日志:

2024-03-22 15:36:15.303 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 1
2024-03-22 15:36:15.305 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 2
2024-03-22 15:36:15.326 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 3
2024-03-22 15:36:15.337 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 4
2024-03-22 15:36:15.344 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 5

这是正常的,每个控件范围内都执行了。我们点击,button, 改变了 name 状态。打印如下日志:

2024-03-22 15:37:48.480 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 1
2024-03-22 15:37:48.480 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 2
2024-03-22 15:37:48.491 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 4

首先我们 name 这个状态影响的组件时 Text,它所在的作用域应该是 Column 内部。打印 RecompositionTest - 2 好理解,可为什么连 Column 的上一级作用域 Box 也被调用了,并且连该 Box 的统计 Box 也被调用了,但是 Card 却又没有被调用。这个好像与上面说的原则相悖。其实不然,我们看看 ColumnBoxCard 源码就清楚了。

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}
@Composable
fun Card(
    modifier: Modifier = Modifier,
    shape: Shape = CardDefaults.shape,
    colors: CardColors = CardDefaults.cardColors(),
    elevation: CardElevation = CardDefaults.cardElevation(),
    border: BorderStroke? = null,
    content: @Composable ColumnScope.() -> Unit
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = colors.containerColor(enabled = true),
        contentColor = colors.contentColor(enabled = true),
        tonalElevation = elevation.tonalElevation(enabled = true),
        shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
        border = border,
    ) {
        Column(content = content)
    }
}

不难发现, Column 和 Box 都是使用 inline 修饰的。
最后简单了解下 Compose 重组的底层原理。
经过 Compose 编译器处理后的 Composable 代码在对 State 进行读取时,能够自动建立关联,在运行过程中,当 State 变化时, Compose 会找到关联的代码块标记为 Invalid, 在下一渲染帧到来之前,Compose 触发重组并执行 invalid 代码块, invalid 代码块即下一次重组的范围。能够被标记为 Invalid 的代码必须是非 inline 且无返回值的 Composable 函数或 lambda。

需要注意的是,重组的范围,与只能跳过并不冲突,确定了重组范围,会调用对应的组件代码,但是当参数没有变化时,在运行时不会真正执行,会跳过本次重组。

四、参数类型的稳定性

4.1 稳定和不稳定

前面,Composable 状态变化触发重组,状态变化基于 equals 比较结果,这是不准确的。准确地说:只有当比较的状态对象,是稳定的,才能通过 equals 比较结果确定是否重组。什么叫稳定的?还是看一个例子:

data class Hero(var name: String)

val shangDan = Hero("吕布")

@Composable
fun StableTest() {
    var greeting by remember {
        mutableStateOf("hello, 鲁班")
    }

    Column {
        Log.i("sharpcj", "invoke --> 1")
        Text(text = greeting)
        Button(onClick = {
            greeting = "hello, 鲁班大师"
        }) {
            Text(text = "搞错了,是鲁班大师")
        }
        ShangDan(shangDan)
    }
}

@Composable
fun ShangDan(hero: Hero) {
    Log.i("sharpcj", "invoke --> 2")
    Text(text = hero.name)
}

运行一下,打印

2024-03-22 17:07:50.248 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:07:50.272 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

点击 Button,再次看到打印:

2024-03-22 17:07:53.182 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:07:53.191 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

问题来了, Shangdan 这个组件依赖的只依赖一个参数,并且参数也没有改变,为什么确在重组过程中被调用了呢?
接下来,我们将 Hero 这个类做点改变,将其属性声明由 var 变成 val

data class Hero(val name: String)

再次运行,

2024-03-22 17:35:41.435 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:35:41.458 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

点击button:

2024-03-22 17:35:47.790 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1

这次,Shangdan 这个 Composable 没有参与重组了。为什么会这样呢?

其实是在因为此前,用 var 声明 Hero 类的属性时,Hero 类被 Compose 编译器认为是不稳定类型。即有可能,我们传入的参数引用没有变化,但是属性被修改过了,而 UI 又确实需要显示修改后的最新值。而当用 val 声明属性了,Compose 编译器认为该对象,只要对象引用不要变,那么这个对象就不会发生变化,自然 UI 也就不会发生变化,所以就跳过了这次重组。
常用的基本数据类型以及函数类型(lambda)都可以称得上是稳定类型,它们都不可变。反之,如果状态是可变的,那么比较 equals 结果将不再可信。在遇到不稳定类型时,Compose 的抉择是宁愿牺牲一些性能,也总好过显示错误的 UI。

4.2 @Stable 和 @Immutable

上面讲了稳定与不稳定的概念,然而实际开发中,我们经常会根据业务自定义 data class, 难道用了 Compose, 虽然 Kotlin 编码规范,强调尽量使用 val, 但是还是要根据实际业务,使用 var 来定义可变属性。对于这种类型,我们可以为其添加 @Stable 注解,让编译器将其视为稳定类型。从而发挥智能重组的作用,提升重组的性能。

@Stable
data class Hero(var name: String)

这样,Hero 即便使用 var 声明属性,它作为参数传入 Composable 中,只要对象引用没变,都不会触发重组。所以具体什么时候使用该注解,还需要根据需求灵活使用。

除了 @Stable,Compose 还提供了另一个类似的注解 @Immutable,与 @Stable 不同的是,@Immutable 用来修饰的类型应该是完全不可变的。而 @Stable 可以用在函数、属性等更多场景。使用起来更加方便,由于功能叠加,未来 @Immutable 有可能会被移除,建议优先使用 @Stable

最后总结一下:本文接着上篇文章的状态,讲解了重组的一些特性,如何确定重组的范围,以及重组的中的类型稳定性概念,以及如何提升非稳定类型在重组过程中的性能。
下一篇文章将会讲解 Composable 的生命周期以及重组的副作用函数。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习|Pytorch】torchvision.datasets.ImageFolder详解

ImageFolder详解 1、数据准备2、ImageFolder类的定义transforms.ToTensor()解析 3、ImageFolder返回对象 1、数据准备 创建一个文件夹&#xff0c;比如叫dataset&#xff0c;将cat和dog文件夹都放在dataset文件夹路径下&#xff1a; 2、ImageFolder类的定义 class ImageFol…

C# WPF编程-元素绑定

C# WPF编程-元素绑定 将元素绑定到一起绑定表达式绑定错误绑定模式代码创建绑定移除绑定使用代码检索绑定多绑定绑定更新绑定延时 绑定到非元素对象Source属性RelativeSource属性DataContent属性 数据绑定是一种关系&#xff0c;该关系告诉WPF从源对象提取一下信息&#xff0c;…

296个地级市GDP相关数据集(2000-2023年)

01、数据简介 GDP&#xff0c;即国内生产总值&#xff08;Gross Domestic Product&#xff09;&#xff0c;是指一个国家或地区所有常住单位在一定时期内生产活动的最终成果。 名义GDP&#xff0c;也称货币GDP&#xff0c;是指以生产物品和劳务的当年销售价格计算的全部最终产…

OpenHarmony实战:CMake方式组织编译的库移植

以double-conversion库为例&#xff0c;其移植过程如下文所示。 源码获取 从仓库获取double-conversion源码&#xff0c;其目录结构如下表&#xff1a; 表1 源码目录结构 名称描述double-conversion/cmake/CMake组织编译使用到的模板double-conversion/double-conversion/源…

南京大学提出用于大模型生成的动态温度采样法,简单有效!

在自然语言处理&#xff08;NLP&#xff09;的领域&#xff0c;大语言模型&#xff08;LLMs&#xff09;已经在各种下游语言任务中展现出了卓越的性能。这些任务包括但不限于问答、摘要、机器翻译等。LLMs的强大能力在于其生成的文本质量和多样性。为了控制生成过程&#xff0c…

力扣由浅至深 每日一题.22 移除链表元素

迄今为止的生命里 —— 24.4.4 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2…

【子集回溯】Leetcode 78. 子集 90. 子集 II

【子集回溯】Leetcode 78. 子集 90. 子集 II 78. 子集90. 子集 II ---------------&#x1f388;&#x1f388;78. 子集 题目链接&#x1f388;&#x1f388;------------------- 78. 子集 class Solution {List<List<Integer>> result new ArrayList<>()…

基于约束求解器对“火影忍者Online”进行智能布阵

文章目录 1. 游戏背景2. 确定决策边界3. 布阵数据3.1 追击状态3.2 角色信息3.3 个性化要求 4. 智能布阵模型4.1 主要的决策变量4.2 约束条件&#xff08;含辅助决策变量&#xff09;4.3 目标函数及求解 1. 游戏背景 今天将以“火影忍者Online”为案例&#xff0c;写一个智能布…

STM32工程 如何设置堆栈大小(Heap和Stack)

方法1&#xff1a;通过CubeMX、CubeIDE 配置 方法2&#xff1a;直接在启动文件中修改 &#xff08;适合所有Keil工程&#xff09; Heap、Stack的值大小&#xff0c;不管使用哪种开发环境&#xff0c;它俩都肯定在启动文件中。 可以通过CtrlF&#xff0c;搜索: Heap&#xff0…

【Linux】从零认识文件操作

送给大家一句话&#xff1a; 要相信&#xff0c;所有的不美好都是为了迎接美好&#xff0c;所有的困难都会为努力让道。 —— 简蔓《巧克力色微凉青春》 开始理解基础 IO 吧&#xff01; 1 前言2 知识回顾3 理解文件3.1 进程和文件的关系3.2 文件的系统调用openwrite文件 fd 值…

STL常用容器(2)---vector容器

1.1 vector基本概念 功能&#xff1a; vector数据结构和数组非常相似&#xff0c;也称为单端数组 vector与普通数组区别&#xff1a; 不同之处在于数组是静态空间&#xff0c;而vector可以动态扩展 动态扩展&#xff1a; 并不是在原空间之后的续接的新空间&#xff0c;而…

如何从 Android 和 iPhone 中的 SIM 卡恢复已删除的联系人 [新]

在手机上&#xff0c;我们经常添加联系人&#xff0c;而很少关心联系人是存储在SIM卡中还是手机中。当我们错误删除SIM卡联系人&#xff0c;或者不当取出插入的SIM卡插入新手机时&#xff0c;那些因业务需要而添加的联系人就会消失。这可能会令人沮丧和困惑。因此&#xff0c;您…

UniApp 应用发布到苹果商店指南

&#x1f680; 想要让你的 UniApp 应用在苹果商店亮相吗&#xff1f;别着急&#xff0c;让我来带你一步步完成这个重要的任务吧&#xff01;在这篇博客中&#xff0c;我将详细介绍如何将 UniApp 应用顺利发布到苹果商店&#xff0c;让你的应用跻身于苹果生态之中。 引言 &…

Python向带有SSL/TSL认证服务器发送网络请求小实践(附并发http请求实现asyncio+aiohttp)

1. 写在前面 最近工作中遇到这样的一个场景&#xff1a;给客户发送文件的时候&#xff0c;为保证整个过程中&#xff0c;文件不会被篡改&#xff0c;需要在发送文件之间&#xff0c; 对发送的文件进行签名&#xff0c; 而整个签名系统是另外一个团队做的&#xff0c; 提供了一…

银行数字化转型导师坚鹏:银行数字化转型必知的3大客户分析维度

银行数字化转型需要进行客户分析&#xff0c;如何进行客户分析呢&#xff1f;银行数字化转型导师坚鹏认为至少从客户需求分析、客户画像分析、客户购买行为分析3个维度进行客户分析。 1.客户需求分析 银行数字化转型需要了解客户需求&#xff0c;不同年龄段的客户有不同的需求…

游戏APP如何提高广告变现收益的同时,保证用户留存率?

APP广告变现对接第三方聚合广告平台主要通过SDK文档对接&#xff0c;一些媒体APP不具备专业运营广告变现的对接能力和资源沉淀&#xff0c;导致APP被封控&#xff0c;设置列入黑名单&#xff0c;借助第三方聚合广告平台进行商业化变现是最佳选择。#APP广告变现# 接入第三方平台…

VGG网络模型

VGG网络模型 VGG的网络架构VGG16VGG19 特点总结时间关系AlexNet和VGG相似之处AlexNet和VGG不同之处启发与影响总结 VGG&#xff08;Visual Geometry Group&#xff09;是由牛津大学的 Visual Geometry Group 提出的一个深度卷积神经网络模型&#xff0c;它在2014年的ImageNet大…

哲♂学家带你深♂入了解动态顺序表

前言&#xff1a; 最近本哲♂学家学习了顺序表&#xff0c;下面我给大家分享一下关于顺序表的知识。 一、什么是顺序表 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储。在数组 上完成数据的增删查改。 顺序表&#xff…

动态规划刷题(算法竞赛、蓝桥杯)--乌龟棋(线性DP)

1、题目链接&#xff1a;[NOIP2010 提高组] 乌龟棋 - 洛谷 #include <bits/stdc.h> using namespace std; const int M41; int f[M][M][M][M],num[351],g[5],n,m,x; //f[a][b][c][d]表示放a个1b个2c个3d个4的总得分 int main(){scanf("%d %d",&n,&m)…

创新指南|贝恩的产品经理RAPID框架:解决问题的分步指南,使决策过程既高效又民主

您是否曾发现自己陷入项目的阵痛之中&#xff0c;决策混乱、角色不明确、团队成员之间的冲突不断升级&#xff1f;作为产品经理&#xff0c;驾驭这艘船穿过如此汹涌的水域可能是令人畏惧的。应对这些挑战的关键在于采用清晰、结构化的决策方法。输入贝恩的 RAPID 框架&#xff…