Compose是声明式UI,但并不是因为其是声明式UI,所以就实现了响应式

var content = "wyz"
@Composable
fun contentText() {Text(content)//这里当content发生改变时,Text不会发生重新刷新
}

Compose的响应来自State这个工具的配合

val content by mutableStateOf("wang")
@Composable
fun ReFresh() {Text(content)//有了State的配合,当content的内容发送改变时,Text会自动刷新
}

对比Livedata

  • LiveData也是数据绑定
  • LiveData数据绑定的是行为(比如当数据更新的时候,绑定setText()这个行为来更新界面)
  • Compose State直接绑定界面本身,声明式UI,非响应式,一劳永逸,节省了大量的关于UI的繁复的代码,以及无需那些mvvm,mvp等为了解决逻辑层和UI层交互问题的框架了

原生Android无法做到:指定某行代码,在运行时重新执行一次

Compose 配合State被代理的变量可以做到

State的作用只是用来监听的,当其包裹的内容发送改变时,会通知使用到它的Compose空间进行局部刷新

State会对被代理的内容的get和set方法加钩子,监听其变化

而局部刷新的功能与State无关(只是通知作用),是Compose实现

黑魔法的原理:

Compose编译器插件会修改代码的逻辑

把可能要重新执行的代码包起来,加上一个返回值

返回值就包括着原先代码块的代码,

然后把这段代码块(同一层级所有@Compose函数)存起来,

当代码块的条件达成的时候(入参改变发生时)重新用新的入参去执行代码块,得到新的值,更新相应代码块

var name by mutableStateOf("wyz")
setContent {Text(name)//就是可能需要重新执行的标志(@Composeable方法传入了一个变量)/*TODO*/
}
var name by mutableStateOf("wyz")
setContent {Text(name)//就是可能需要重新执行的标志(@Composeable方法传入了一个变量)/*TODO*/
}

被保存起来的代码(伪代码)

WrapperFun{Text(name)/*TODO*/
}

当name发生改变时,WrapperFun中的代码会被重新执行

Compose 重复的刷新叫做Recomposition

Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state 变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来之前 Compose 会触发 recomposition,并在重组过程中执行 invalid 代码块。

Invalid 代码块即编译器找出的下次重组范围。能够被标记为 Invalid 的代码必须是非 inline 且无返回值的 @Composalbe function/lambda,必须遵循 重组范围最小化 原则。

不需要Recomposition的地方加上remember,把数据缓存起来,不需要重复执行

val name = mutableStateOf("wyz")
var textNum=1
@Composable
fun testView(){Column {Text(name.value, Modifier.clickable {name.value="wang"textNum++//这里的变量虽然不是State,当时下面那个Text与上面属于同一个层级,Column下面的控件//所以当name触发了第一个Text reCompose时,第二个Text也属于第一个Text重新刷新时候要执行的代码块//所以两个Text都会重新刷新})Text(textNum.toString()) //点击第一个Text,这里也会刷新//流程是 Text(1)click-》// name改变-》// Text(1)同一层级下代码块重新执行-》// Text(2)也被重新刷新}
}

Compose控件是无状态的

如果不是外部传入的参数,Compose组件内部是拿不到这个组件内的局部变量的

因为Compose有别于传统的控件,

个人理解:传统View是类,能生成实例,Compose都是函数,执行完就完了,没有生成实例,

这个特性的特点就是无状态(缺点:没法拿到一个实例的某个变量,优点:可以局部ReCompose,局部更新)

想要把某个控件的状态共享出去,只能把状态作为参数,又由外层传入,相当于共享给外部

如果最外层要得到这个参数,只能一层层传出去,

@Composable
fun father(){Box {son()//Compose无状态的特性,这里拿不到son的任何属性}
}@Composable
fun son() {val content = "wyz"Text(content)
}
@Composable
fun son(sonContent: String) {//如果Text的content需要让外部拿到,需要把它通过参数传入,抛给外层Text(sonContent)
}@Composable
fun father() {Box {val sonContent = "wyz"son(sonContent)print(sonContent)//这样就拿到了子控件的属性"content"}
}

这里的原则是参数能不往外提就尽量收拢在控件内部,就像变量设置private一样

State 就是对内容的代码,当被外部调用set的时候,对所有调用其get方法的Compose代码块出发reCompose

但是以下代码有问题:(不会触发任何刷新)

val nums = mutableStateOf(mutableListOf("one","two","three"))
@Composable
fun testView(){Column {Button(onClick ={nums.add(nums.last()+1)//实际上点击了Button后nums被调用的是add而不是set,所以下面那个“ListView”(nums并不会触发重新刷新,界面丝毫未发生改变)}){Text("加上1")}for (num in nums){Text(num)}}
}

用到list时候,要使用mutableStateListOf

map用到mutableStateMapOf

val nums = mutableStateListOf("one","two","three")//mutableStateListOf用来包裹list,当被调用add或者remove时也会触发刷新
@Composable
fun ListView(){Column {Button(onClick ={nums.add(nums.last()+1)}){Text("加上1")}for (num in nums){Text(num)}}
}

官方为我们默认做好的Recompose的优化:

编译器插件会默认为@Composable函数 插入一个判断条件,判断@Composable函数的入参是否发生改变

如果不变,那该函数不会触发ReCompose

好处:在同一层级其他Compose出发ReCompose时,不变的Compose控件避免不必要的刷新

当然,这个只是消除了自动刷新带来的性能损耗(是消除与传统UI的性能差距,不是做到比传统性能更好)

val name = mutableStateOf("wyz")
var textNum=1
@Composable
fun testView(){Column {Text(name.value, Modifier.clickable {name.value="wang"textNum++//这里的变量虽然不是State,当时下面那个Text与上面属于同一个层级,Column下面的控件//所以当name触发了第一个Text reCompose时,第二个Text也属于第一个Text重新刷新时候要执行的代码块//所以两个Text都会重新刷新})Heavy()//点击第一个Text,这里不会刷新Text(textNum.toString()) //点击第一个Text,这里也会刷新}
}
@Composable
fun Heavy(){//由于没有入参,同层级其他控件触发ReCompose时这个函数会跳过重新执行Text("aaa")//
}

判断一个@Composable函数是否会被动刷新的规则比较复杂(这里还漏了很多细节),需要后续进行补充,详情看Compose第六讲

以下写法,如果入参发生改变也不会出发ReCompose

@Composable
fun Heavy(content:Int){//由于入参没有用到,同层级其他控件触发ReCompose时这个函数会跳过重新执行Text("aaa")
}

但是这里还有个问题要后续思考:

如果是这样呢:

val name = mutableStateOf("wyz")
var textNum=1
@Composable
fun testView(){Column {Text(name.value, Modifier.clickable {name.value="wang"textNum++//这里的变量虽然不是State,当时下面那个Text与上面属于同一个层级,Column下面的控件//所以当name触发了第一个Text reCompose时,第二个Text也属于第一个Text重新刷新时候要执行的代码块//所以两个Text都会重新刷新})Heavy()//这里会不会重新刷新Text(textNum.toString()) //点击第一个Text,这里也会刷新}
}@Composable
fun Heavy(){//由于没有入参,同层级其他控件触发ReCompose时这个函数会跳过重新执行吗?Text(textNum.toString()))//这里的值实际上已经发生了改变
}

Compose 在编译期分析出会受到某 state 变化影响的代码块,并记录其引用,当此 state 变化时,会根据引用找到这些代码块并标记为 Invalid 。在下一渲染帧到来之前 Compose 会触发 recomposition,并在重组过程中执行 invalid 代码块。

Invalid 代码块即编译器找出的下次重组范围。能够被标记为 Invalid 的代码必须是非 inline 且无返回值的 @Composalbe function/lambda,必须遵循 重组范围最小化 原则。

为何是 非 inline 且无返回值(返回 Unit)?

对于 inline 函数,由于在编译期会在调用处中展开,因此无法在下次重组时找到合适的调用入口,只能共享调用方的重组范围。

而对于有返回值的函数,由于返回值的变化会影响调用方,因此无法单独重组,而必须连同调用方一同参与重组,因此它不能作为入口被标记为 invalid

inline的陷阱

@Composable
fun Foo() {var text by remember { mutableStateOf("") }Button(onClick = { text = "$text $text" }) {Log.d(TAG, "Button content lambda")Box {Log.d(TAG, "Box")Text(text).also { Log.d(TAG, "Text") }}}
}

日志如下

D/Compose: Button content lambda
D/Compose: Boxt
D/Compose: Text

为什么重组范围不是从Box开始?

Column、Row、Box 乃至 Layout 这种容器类 Composable 都是 inline 函数,因此它们只能共享调用方的重组范围,也就是 Button 的 尾lambda

如果你希望通过缩小重组范围提高性能怎么办?

@Composable
fun Foo() {var text by remember { mutableStateOf("") }Button(onClick = { text = "$text $text" }) {Log.d(TAG, "Button content lambda")Wrapper {Text(text).also { Log.d(TAG, "Text") }}}
}@Composable
fun Wrapper(content: @Composable () -> Unit) {Log.d(TAG, "Wrapper recomposing")Box {Log.d(TAG, "Box")content()}
}

通过非内联的形式,使之满足 Compose 重组范围最小化条件。

Compose系列:Recomposition重组作用域相关推荐

  1. Jetpack Compose 中的重组作用域和性能优化

    只有读取可变状态的作用域才会被重组 这句话的意思是只有读取 mutableStateOf() 函数生成的状态值的那些 Composable 函数才会被重新执行.注意,这与 mutableStateOf ...

  2. 了解 Jetpack Compose 中的重组

    对事件变化的响应对于创建最佳用户界面很重要.在许多情况下,用户不会输入正确的内容,并且当用户更改首选项或位置时,UI 必须更改. 为了使用户界面保持最新,UI 框架 Jetpack Compose 提 ...

  3. Compose系列 四 生命周期

    本系列是我学习compose过程中,对官方文档的翻译和解读,以及实验性的Demo工程.主要参考官方文档和中文手册 全部的正文内容(Demo工程除外)源自Compose官方文档,个人解读以引用的形式插入 ...

  4. 前端学习(1700):前端系列javascript之作用域和自由变量

  5. Jetpack Compose 深入探索系列一:Composable 函数

    Composable 函数的含义 如果我们只专注于简单的语法,任何标准的Kotlin函数都可以成为一个可组合函数,只需将其注解为@Composable: 通过这样做,我们实际上是在告诉编译器,该函数打 ...

  6. 揭秘 Compose 原理,理解 Composable 的本质

    你好,我是朱涛. 今年的Google I/O大会上,Android官方针对Jetpack Compose给出了一系列的性能优化建议,文档和视频都已经放出来了.总的来说,官方的内容都非常棒,看完以后我也 ...

  7. 学不动了,尝试用Android Jetpack Compose重写微信经典飞机大战游戏

    前段时间看了TechMerger大佬写的<一气呵成:用Compose完美复刻Flappy Bird!>,甚是有趣,按耐不住那躁动的心,笔者决定跟随大佬的脚步通过写游戏的方式学习Jetpac ...

  8. Compose 学习笔记(一)—— Compose 初探

    历时两年,Android 团队推出了全新的原生 Android 界面 UI 库--Compose.当然,Compose 也是属于 Jetpack 工具库中的一部分,官方宣称可以简化并加快 Androi ...

  9. Android | Compose 初上手

    简介 Jetpack Compose 是用于构建原生 Andorid 界面的新工具包,Compose 使用了更少的代码,强大的工具和直观的 Kotlin Api 简化并且加快了 Android 上界面 ...

最新文章

  1. 1、SELECT:数据表查询语句
  2. java多态调用优先级_关于java的多态方法调用顺序的问题
  3. Activity生命周期 onCreate onResume onStop onPause
  4. java分数怎么保存到文件_Java如何将控制台上的结果保存到文件
  5. 号和管道符号(|)在不同场景下的使用方法
  6. JavaScript动态设置table的高度
  7. 各种排序(数据结构复习之内部排序算法总结)
  8. 6 volist双层数组_Javascript算法 — 数组排序
  9. linux虚拟主机_云服务器与虚拟主机的区别
  10. LAMP集群项目五 nfs存储的数据实时同步到backupserver
  11. Docker 入门实践
  12. JavaSE进阶582-589 IO流(二)File类/目录拷贝
  13. 模2除法(CRC检验码)
  14. 提高电商ROI的方法有哪些?
  15. 电子商务专业英语参考试卷
  16. Batch Normalization反方向传播求导
  17. 解密微信小程序加密的微信运动数据(java)
  18. java实现在pdf中添加文字和图片
  19. 怎样用计算机给ipd传电影,怎样不使用iTunes将电影导入iPad?
  20. GNS3-GREvpn

热门文章

  1. PMP 考试工具技术常见翻译问题
  2. AI之机器翻译及相关技术
  3. 华为OD机试 - 射击比赛(JavaScript) | 机试题+算法思路+考点+代码解析 【2023】
  4. 谁有软件开发由航母最后变小破船的搞笑图
  5. 最热门的十大P2P软件排行
  6. java动态表单设计解析
  7. Php全套教程,Php学习路线图
  8. Android 避免Overdraw 过度绘制
  9. Virtual Studio 2019 激活码
  10. 深入理解Linux网络技术内幕 第21章 传输