Jetpack Compose 初了解
Compose 独立于平台的真实含义是:它的上层完全不依赖 Android,但底层的实现还是需要 Android 的 API 去做。
矢量图(Vector Graphics)只是描述规则,不需要将真实像素表达出来,特点是占用空间小、不怕放大。矢量图不会失真,原因如下:
矢量图为什么不会失真?
- 基于数学公式:矢量图使用点、线、曲线和多边形等基本几何元素,由数学方程精确定义。例如,直线用两个点的坐标和方向描述,曲线用贝塞尔曲线公式表示。
- 分辨率独立:由于是基于数学定义,矢量图与设备分辨率无关。放大或缩小时,软件会重新计算方程,图像质量不会受影响。
- 无像素限制:矢量图不存在固定像素网格,放大时不会出现像素化(锯齿状边缘)。位图则会因为像素拉伸而失去清晰度。
- 可无限缩放:矢量图可以无限放大或缩小,不影响清晰度和细节,适合用于 LOGO、字体、插图等设计。
Compose 组件 API 与安卓原生的对应关系
| 传统 View 系统 | Jetpack Compose | 说明/补充 |
|---|---|---|
TextView | Text() | 完全对应,Compose 的 Text() 更灵活 |
ImageView | Image() | 对应,Compose 可直接用 Image 渲染图片 |
FrameLayout | Box() | 对应,Box 是单层布局,可以重叠放置子元素 |
LinearLayout | Column() / Row() | 对应,Column 纵向,Row 横向布局 |
RelativeLayout | Box() (仅作简单重叠) | 不完全等价,Compose 的 Box 只支持简单重叠 |
ConstraintLayout | ConstraintLayout() | Compose 也有 ConstraintLayout,需要加依赖 |
MotionLayout | MotionLayout() | Compose 支持 MotionLayout(Accompanist/官方支持) |
ScrollView | Column(Modifier.verticalScroll()) | 横向用 Row + Modifier.horizontalScroll() |
RecyclerView | LazyColumn() / LazyRow() | 对应,Compose 懒加载组件,效率高 |
ViewPager2 | Pager() | Pager 在 Accompanist/Compose Foundation |
具体说明
-
RelativeLayout ≠ Box
- Box 只支持“简单的重叠”,不支持 RelativeLayout 那种“依赖兄弟 View 位置” 的复杂关系。
- 如果你要类似“左对齐、右对齐、某个 View 在另一个下方”这种,建议用 ConstraintLayout for Compose(需要依赖:
implementation "androidx.constraintlayout:constraintlayout-compose:...")。
-
ConstraintLayout、MotionLayout
- Compose 原生就有 ConstraintLayout,API 比传统略有不同,但能力对等甚至更强。
- MotionLayout 目前 Compose 也支持,但还没像 View 那样完善,大多数场景已经够用。
-
ScrollView → Column/Row + Scroll 修饰符
- Compose 没有“ScrollView”组件,而是给 Column/Row 添加
Modifier.verticalScroll()或Modifier.horizontalScroll()实现可滚动。
- Compose 没有“ScrollView”组件,而是给 Column/Row 添加
-
RecyclerView → LazyColumn/LazyRow
- Compose 的 LazyColumn/LazyRow 专门为高效长列表设计,是 RecyclerView 的直接替代。
-
ViewPager → Pager
- Pager 是 Compose 的分页组件,最初在 Accompanist,现在已经集成到 Compose Foundation 中(
androidx.compose.foundation.pager),用法类似 ViewPager2。
- Pager 是 Compose 的分页组件,最初在 Accompanist,现在已经集成到 Compose Foundation 中(
小结
- Box ≠ RelativeLayout,复杂布局用 ConstraintLayout
- LazyColumn/LazyRow 完全可以替换 RecyclerView
- Compose 没有单独的 ScrollView,而是 Modifier + 布局组合
- MotionLayout/ConstraintLayout 在 Compose 也有,需单独依赖
写法举例
// FrameLayout
Box { ... }
// LinearLayout
Column { ... } // 纵向
Row { ... } // 横向
// RelativeLayout
// 用 ConstraintLayout for Compose 替代复杂场景
ConstraintLayout { ... }
// ScrollView
Column(Modifier.verticalScroll(rememberScrollState())) { ... }
// RecyclerView
LazyColumn { items(100) { ... } }
// ViewPager
HorizontalPager(count = 5) { page -> ... } // 需添加依赖
尺寸对应关系
-
不写尺寸:
在 Compose 里,如果不给组件加尺寸相关 Modifier(比如Modifier.width(),Modifier.height(),fillMaxWidth()),它默认的宽高策略就是“包裹内容” ,类似于 Android XML 里的wrap_content。比如:
Text("Hello") // 等价于 XML 里的 <TextView ... android:layout_width="wrap_content" android:layout_height="wrap_content"/> -
match_parent 相当于 Modifier.fillMaxWidth
如果要像 XML 里match_parent那样,让控件宽度/高度充满父布局,就要用对应的 Modifier:Modifier.fillMaxWidth() // 宽度填满 Modifier.fillMaxHeight() // 高度填满 Modifier.fillMaxSize() // 宽高都填满这就等同于 XML 的
match_parent效果。
通用的方式用Modifier;专项的参数用函数参数
-
通用属性用 Modifier
像大小、颜色、间距、点击、边框、透明度、阴影、动画等,Compose 都推荐通过Modifier实现,这样所有组件都能通用同一套修饰符。例子:
Text( text = "内容", modifier = Modifier .padding(8.dp) .background(Color.Red) .clickable { ... } ) -
专项/专用属性用函数参数
有些属性是某个组件特有的功能,不能用 Modifier 通用表达,还是用参数,比如 Text 的maxLines, Button 的onClick,Image 的contentScale:Text( text = "标题", maxLines = 1, // 专用参数 overflow = TextOverflow.Ellipsis, // 专用参数 modifier = Modifier.padding(4.dp) ) Button( onClick = { /* 专用参数 */ }, modifier = Modifier.padding(8.dp) ) { Text("按钮") } -
官方设计理念
Modifier 负责“通用、可组合、可以链式组合的修饰”,组件参数负责“专用功能、表达语义的属性”。
总结口诀:**
- 尺寸/位置/外观/行为 → Modifier
- 语义/特性/内容 → 组件参数
Click排列不同会导致点击响应区域出现差异。
在 Compose 中,Modifier 的顺序非常重要。 比如下面两种写法,Modifier.padding().background().clickable() 和Modifier.clickable().padding().background(),它们的实际点击响应区域是不一样的。
示例 1
// 写法A
Box(
Modifier
.padding(16.dp)
.background(Color.Red)
.clickable { /* ... */ }
)
点击区域:
padding先加了外边距,然后才是背景和点击区域。- 只有 中间有颜色的部分(内容+内边距)可以被点击,padding 空白处点不到。
示例 2
// 写法B
Box(
Modifier
.clickable { /* ... */ }
.padding(16.dp)
.background(Color.Red)
)
点击区域:
clickable最先加在外层。- 整个 Box,包括 padding 区域都可以被点击,但 padding 区域没有背景色,看起来像“点空气也能响应”。
本质原理
- Modifier 是链式调用,顺序就是应用顺序。
- clickable 的响应区域 = 它后面修饰器最终确定的“内容尺寸” 。
- 把 clickable 放前面,等于点大盒子(包括 padding);放后面,等于点实际内容(不含 padding)。
为什么需要Compose?
Compose 是为了解决传统 UI 系统的开发效率低下、可维护性差、性能不足等问题,让 Android UI 开发变得更现代、更高效、更符合声明式编程的潮流。
Compose 将 UI 转化为绘图指令,绘图指令交给 Skia。Skia 写入 buffer(图形缓冲区),而 buffer 实际就是二维像素数组。每个像素对应一个 xy 坐标,保存 ARGB 值。Skia 把指令变为屏幕能识别的二维像素数据。
每一个UI都会被转化成 LayoutNode,轻量级的东西,而View或者ViewGroup非常重,这样做的目的是为了做复用和拓展。
大量能力“外包”出去,LayoutNode 可以动态组合属性:
- 添加 text 属性就是 Text
- 添加 image 属性就是 Image
- 添加 button 属性就是 Button
所有属性都转化为 Modifier,如 paddingModifier。
AndroidComposeView 相当于“创世主”,是 Compose 世界的起点。
Compose的分层
分层与依赖:上层依赖下层
material(3) 一堆material设计风格组件的包,button、TextField等。floating action button,这些组件都具有一定的固定风格,类似于app的一种主题,在某种主题下,所有的空间都具有某种一致性。
foundation 相对可以用的UI体系,column row、BasicTextFiled等
animation 动画层
ui ui最基础的支持,测量,布局,绘制,等。
runtime compose最底层的机制,数据结构,转化机制等
-·-·-·-·-·-·-·-·-·-·
compile 编译层
Compose的包依赖原则
- 写代码的时候,依赖material(3)就够了,可能跳过material 依赖 foundation就够了。
- 如果需要ui-tooling,预览功能,需要单独把它写出来;
- 如果需要 material-icons-extended,必须要专门列出来。(material3不依赖这个,只依赖material-icons-core,这里边的矢量图远比mater-icons-core多)。
Compose的自定义
Compose 用编译期插件(compiler plugin)处理代码。为什么不是 Annotation Processor 或字节码? 为了跨平台,传统的方式只能用在 JVM 平台。
Compose 用一种更紧凑、一体化的声明方法代替了繁琐的 xml + 自定义 View 代码,Compose 只需要 @Composable 函数,而xml 只是标记语言,只能读,不能执行。
什么是 Composable?
带 @Composable 注解的函数。作用是告诉 Compose 编译器,进行特殊处理,支持声明式 UI 和响应式更新机制。
MutableState 和 mutableStateOf 的区别
常见用法如下:
val name = mutableStateOf("zjj")
val name by mutableStateOf("zjj")
val name by remember { mutableStateOf("zjj")}
这三种方式的主要区别如下:
① val name = mutableStateOf("zjj")
-
使用时需手动调用
.value:name.value = "new value" Text(name.value) -
每次组件重组(recomposition)时,这行代码都会重新执行,导致状态重新赋值为初始值
"zjj",状态无法持久化。 -
不推荐直接使用,除非需要显式地访问状态的值。
② val name by mutableStateOf("zjj")
-
使用 Kotlin 的属性委托(by)语法,自动调用
getValue()和setValue(),无需手动调用.value:name = "new value" Text(name) -
但与第一种方式类似,状态每次重组时仍会重新赋值,无法持久化。
-
推荐在函数内部临时存储时使用,不适合持久状态。
③ val name by remember { mutableStateOf("zjj") }
-
使用
remember包裹状态,仅在首次组合时执行一次初始化,后续重组时保持原有状态:name = "new value" Text(name) -
推荐的状态管理方式,Compose 中标准用法,可持久保存状态,不会因重组而重置为初始值。
底层实现(核心部分):
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
// 读的时候记录被读过了。这里的next就是内部链表的头节点,遍历这个链表,找到最新的可用的StateRecord,并且记录这个StateObject被使用了。因为它才是真正被订阅的对象。而不是链表中的某个节点,使整个链表都被订阅了。
// 1.订阅;2返回最新的可用值。
get() = next.readable(this).value
// 写的时候不仅写,还需要通知之前读过我的地方去刷新。
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
Compose 刷新分为三步:组合(composition) 、布局、绘制。 传统只有布局和绘制(测量包含在布局)。
setContent { // 这个大括号内就是组合过程。之所以取这个名字,是因为这个代码就是拼凑出实际页面的过程。
Text(text = name.value)
Text(text = name.value)
}
这些代码包装到一个 ComposeView 里,由 AndroidComposeView 管理,内部用 LayoutNode 进行布局和绘制。
MutableState为什么能被订阅?
并不是 MutableState 能被订阅,而是它背后的 StateObject。 更底层是 StateRecord。 设计这么多层,是因为 Compose 支持事务(批量更新、撤销等),需要存储历史值。用链表结构保存。
┌─────────────────────────────┐
│ MutableState<T> │
│ ── 我们在代码里用到的状态对象 │
│ (val count = mutableStateOf(0)) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ StateObject │
│ (管理一组 StateRecord 的头) │
└─────────────┬───────────────┘
│
firstStateRecord
│
▼
┌──────────┬─────────────┬─────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│StateRec│ │StateRec│… │StateRec│… │StateRec│
│ ord #1 │ │ ord #2 │ │ ord #N │ │ ord #M │
└─────┬──┘ └─────┬──┘ └─────┬──┘ └─────┬──┘
│ │ │ │
snapshotId snapshotId snapshotId snapshotId
= 101 = 102 = 103 = 110
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────────────────────────────────────┐
│ Snapshot │
│ (每个事务/批量修改/回滚都生成一个快照) │
│ snapshotId = 101、102、103、110 ... │
│ │
│ - 记录了哪些 StateObject 被读/写 │
│ - 记录了哪些 Composable 需要重组 │
│ - 支持事务、撤销、合并 │
└───────────────────────────────────────────────┘
为什么要设计三层结构(MutableState → StateObject → StateRecord)?
-
一般变量只存当前值就够了,为什么 Compose 这么复杂?
因为 Compose 不只是要保存“最新值”,还要支持“事务”功能,比如:- 批量更新多个状态后一次性提交(类似数据库的事务)
- 支持撤销、回滚(回到某个历史状态)
-
为什么需要保存“历史值”?
只有存下历史值,才能让事务或撤销成为可能。例如,用户操作时如果需要撤回操作,框架就能恢复到某个历史快照。
如何实现对历史状态的管理?
-
用什么方式存多个历史值?
- Compose 用链表结构来保存每个变量在不同时刻的值(每次变更就新增一个节点)。
- 这个链表的头节点叫
firstStateRecord,每个节点就是一个StateRecord,带有快照 id 和当时的值。
-
三层结构的意义:
MutableState:你看到/用到的外层“可变状态”对象。StateObject:内部实际“变量历史”的管理器,持有链表。StateRecord:链表的每个节点,记录着变量在某个快照下的值。
Compose 要支持事务和回滚,就得保存每个变量的历史版本。它用链表把所有历史状态串起来,每次变更就加个新节点,链表的头就是最新的。你用的是 MutableState,底下其实是一套 StateObject + StateRecord 的链表结构,负责管理所有版本的值。
// 创建一个可观察的可变状态对象(MutableState)
// 支持快照策略(默认结构性相等),用于 Compose 响应式重组机制
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
// StateObject 代表一个状态对象,负责管理所有历史版本(链表结构)
interface StateObject {
// 链表头,指向当前状态的第一个历史记录(最新的 StateRecord)
val firstStateRecord: StateRecord
// 在链表头部插入一个新的 StateRecord(例如变量值发生变化时)
fun prependStateRecord(value: StateRecord)
// 合并多个 StateRecord,处理快照合并或事务提交场景(可选实现)
fun mergeRecords(
previous: StateRecord,
current: StateRecord,
applied: StateRecord
): StateRecord? = null
}
// StateRecord 表示某个状态在某一次快照下的值(链表节点)
abstract class StateRecord {
// 记录这个节点属于哪个快照(Snapshot)的 id
internal var snapshotId: Int = currentSnapshot().id
// 指向下一个历史节点(旧状态)
internal var next: StateRecord? = null
// 从另一个 StateRecord 拷贝属性到自己(常用于创建新快照节点时)
abstract fun assign(value: StateRecord)
// 创建一个新的 StateRecord 实例(通常用于快照机制)
abstract fun create(): StateRecord
}
源码说明:
-
mutableStateOf创建的 MutableState,其实内部维护着一个状态历史链表,每次变更都能保留历史值,方便 Compose 实现撤销/事务。 -
StateObject是所有状态对象的核心,负责管理链表、插入新节点、快照合并等。 -
StateRecord则代表每个版本/快照下的状态值,是实际存值的节点。
// 为 StateRecord 提供可读快照值的方法。
// 作用:在读取 Compose 状态值(如 name.value)时,记录依赖关系,支持快照一致性和自动重组。
fun <T : StateRecord> T.readable(state: StateObject): T {
// 1. 获取当前快照(Snapshot)。快照是 Compose 用来支持多版本状态、批量更新等的事务机制。
val snapshot = Snapshot.current
// 2. 如果有读订阅者(readObserver),通知它“state”被这个快照读取了。
// 比如 UI 里的 Text(text = name.value) 会触发这里。
// 这个通知实际上就是注册订阅,方便后续写入时知道哪些地方要重组。
snapshot.readObserver?.invoke(state)
// 3. 获取当前快照下最新可用的 StateRecord 节点
// (链表查找与快照 id 匹配且有效的节点,如果找到则直接返回)
return readable(this, snapshot.id, snapshot.invalid) ?: sync {
// 4. 如果上一步没找到,进行同步操作(比如多线程情况下,重新获取当前快照并重试一次)
val syncSnapshot = Snapshot.current
@Suppress("UNCHECKED_CAST")
// 5. 再次尝试获取链表中最新的、有效的 StateRecord
readable(state.firstStateRecord as T, syncSnapshot.id, syncSnapshot.invalid) ?: readError()
}
// 6. 如果还是没找到,抛出读取错误
}
源码说明:
- 每次读取
name.value(或其他 Compose 状态值),都会经过这里,自动注册依赖(订阅关系),用于后续的 UI 自动更新。 - 通过快照 id 在链表中查找对应的历史状态,保证在并发、事务、撤销等情况下总能读到正确版本的数据。
- 如果找不到匹配快照,就同步重试,并在极端情况下抛出错误(防止状态异常)。
Compose 状态的订阅机制
1. 多层状态对象
MutableState→StateObject→StateRecord- 目的是支持事务(批量更新、撤销、并发多快照等)——所以状态不仅有当前值,还有历史值(链表结构)。
2. 读写的订阅与通知
- get(读取)时: 注册订阅(
readObserver),记录“谁用了我”。 - set(写入)时: 通知所有依赖我的地方(
writeObserver),触发重组/刷新。
两种订阅机制
-
对 Snapshot(快照)的读写订阅:
- readObserver: 每次
get都会调用,注册依赖。 - writeObserver: 每次
set都会调用,通知相关 UI 更新。
- readObserver: 每次
-
对每个 StateObject 的应用订阅:
- 细粒度追踪,支持局部重组,自动管理哪些组件该更新。
为什么 MutableState 的 value 可以被订阅
MutableState.value的 getter 会自动调用 readable(),并通过readObserver注册“我在这里被读取过”。- 这样后续 set 的时候,
writeObserver就能定位到“谁读了我”,从而通知这些地方重组。 - 这就是 Compose 能实现自动响应式更新的底层基础。
4. set 的过程
- set(value) 时不仅会存新值,还会在链表中加新 StateRecord,打上当前 snapshotId。
- 然后通过依赖追踪机制,把“需要重组的地方”全部标记失效,下帧就会自动重新组合这些区域。
历史值与快照
-
状态(StateObject)里的每个版本都存在一个链表里(
StateRecord)。 -
每个 StateRecord 绑定一个
snapshotId,用于快照机制。 -
这样:
- 能支持事务(比如批量提交/撤销)。
- 能并发多快照(不同线程/协程各自看到自己的状态)。
readable() 的两种用法
-
两个参数: (通常场景)
- 订阅 + 取值 —— 自动注册依赖。
-
三个参数: (内部使用)
- 只取值不订阅(比如快照内部查历史),遍历链表取最新的、可用的 StateRecord。
流程总结
flowchart TD
A(MutableState) --> B(StateObject)
B --> C1(StateRecord #1)
B --> C2(StateRecord #2)
C1 -- snapshotId=1 --> S1[Snapshot #1]
C2 -- snapshotId=2 --> S2[Snapshot #2]
A -- get() --> D{UI使用}
D -- 注册依赖 --> B
A -- set() --> E{通知依赖}
E -- 局部重组 --> D
关键说明:
-
MutableState → StateObject → StateRecord(链表)
MutableState的 getter 调用readable(),底层进入StateObject的StateRecord链表,读取带有当前snapshotId的节点。
-
readObserver / writeObserver
- readObserver 在
get()时被Snapshot调用,用来向SlotTable注册“哪个 Composable(组ID)读取了我”。 - writeObserver 在
set()时被Snapshot调用,用来通知SlotTable把依赖我的那批组ID标记失效。
- readObserver 在
-
SlotTable
- 保存“组ID ↔ Composable 代码位置”的映射。失效后,只重新组合这些被标记的组,而非整个 UI 树。
说明:
MutableState->StateObject->StateRecord->Compose的关系整理
-
每个可变状态(MutableState)背后是一个 StateObject,它包含一个 StateRecord 链表。
-
每次在不同快照(Snapshot)下对状态修改,都会新生成一个 StateRecord 节点,并打上该快照的 snapshotId。
-
每个快照只认自己和祖先快照的 StateRecord(有效性由 snapshotId 范围决定)。
-
快照合并/撤销时,会遍历链表,挑选出“当前快照视角下可见”的那个 StateRecord 作为当前值。
-
这样就实现了多快照隔离、批量合并、撤销等事务性功能。
-
Snapshot ,SlotTable readObserver writeObserver StateRecord 关系总结
┌────────────────────────────────────────────────────────┐
│ Composable (groupA) │
│ (你的 @Composable 函数体,分组 ID = groupA) │
└────────────────────────────────────────────────────────┘
│
│ 1. 读状态:调用 MutableState.value getter
│
▼
┌────────────────────────────────────────────────────────┐
│ MutableState │
│ (exposed to your code) │
└────────────────────────────────────────────────────────┘
│
│ 调用 readable(thisStateObject)
│
▼
┌────────────────────────────────────────────────────────┐
│ StateObject │
│ (管理 StateRecord 链表头) │
└────────────────────────────────────────────────────────┘
│
│ 查链表,找当前 snapshotId 匹配的记录
│
│ ┌────────────────────────┐ ┌────────────────────────┐
│ │ StateRecord (最新) │──▶│ StateRecord (旧) │
│ │ (latest value) │ │ (old value) │
│ └────────────────────────┘ └────────────────────────┘
│
│ 返回 value 给 Composable
│ 并触发 snapshot.readObserver(stateObject)
▼
┌────────────────────────────────────────────────────────┐
│ SlotTable │
│ (依赖登记表) │
│ stateObject → [groupA, groupB,…] 注册 groupA 依赖 │
└────────────────────────────────────────────────────────┘
│
│ 2. 写状态:执行 MutableState.value = newValue
│
▼
┌────────────────────────────────────────────────────────┐
│ StateObject.overwritable │
│ (创建新 StateRecord,打上当前 snapshotId 并插入链表) │
└────────────────────────────────────────────────────────┘
│
│ 完成写入后触发 snapshot.writeObserver(stateObject)
▼
┌────────────────────────────────────────────────────────┐
│ SlotTable │
│ (依赖登记表) │
│ 查出 stateObject 对应的 [groupA, groupB,…] │
└────────────────────────────────────────────────────────┘
│
│ 标记这些 group 为 “失效”
▼
┌────────────────────────────────────────────────────────┐
│ Compose 调度局部重组 (Recompose) │
│ 只重新执行标记为失效的 groupA、groupB… 组 │
└────────────────────────────────────────────────────────┘
│
│ UI 更新完成
▼
┌────────────────────────────────────────────────────────┐
│ 新的 Composable (groupA) │
│ (仅重组了依赖该状态的区块) │
└────────────────────────────────────────────────────────┘
-
MutableState → StateObject → StateRecord(链表)
MutableState是对外暴露的可变状态。StateObject管理一个StateRecord链表,记录每个快照下的值历史。- 每个
StateRecord带有一个snapshotId,关联到某个Snapshot。
-
Snapshot / readObserver / writeObserver
- readObserver:每次在 Composable 中读取状态(
get())时,由当前Snapshot调用,向StateObject注册「当前正在执行的组 ID」。 - writeObserver:每次写入状态(
set())时,由Snapshot调用,标记所有依赖过该状态的组 ID「失效」。
- readObserver:每次在 Composable 中读取状态(
-
SlotTable & 组合
SlotTable维护整个组合树中各个 Composable 调用的「组 ID」与代码位置映射。- 当某个组 ID 被
writeObserver标记失效后,Compose 会通过SlotTable精确定位到对应的 Composable 段落,只重组这一部分,而非整棵树。
这样,Compose 就能做到:状态改变只通知真正依赖的 UI 片段,且重组时快速跳过未变化的部分,既保证了响应式正确性,又实现了高效更新。
- 链表的意义:每个变量的历史状态用 StateRecord 链表保存,每个节点有独立的 snapshotId,实现多事务并发、批量回滚等功能。
- 依赖通知核心:所有依赖于这个状态的 UI 组合,被 writeObserver 标记为“失效”,下一帧会重组,实现响应式数据流。
- policy:决定新旧值是否“等价”。只有变更时才写入,否则不触发重组,优化性能。
- overwritable/overwritableRecord:这是事务隔离和历史回溯的核心,Compose 可以在事务(快照)中独立读写状态,支持撤销、合并等功能。
// Compose 的底层可变状态实现,支持事务和快照机制
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
// 读取当前值时:
// 1. 调用 StateRecord 的 readable(),会自动完成依赖注册(即订阅)。
// 2. 返回当前快照下该变量的最新可用值。
get() = next.readable(this).value
// 赋值时:
// 1. 用 policy 检查新旧值是否等价(如内容没变就不重组)。
// 2. 只有新值和旧值不同才真正写入。
// 3. 通过链表方式,定位到当前快照的 StateRecord 节点,写入新值。
// 4. 写入后自动触发依赖通知(即 UI 失效并重组)。
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
// 写入新值,并触发通知机制
next.overwritable(this, it) { this.value = value /* 新值传给 StateRecord */ }
}
}
}
// 负责“找到可写节点并写入”,并在写入后自动触发依赖更新
internal inline fun <T : StateRecord, R> T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
// 拿到当前快照下可写的 StateRecord,然后执行 block 写入逻辑
this.overwritableRecord(state, snapshot, candidate).block()
}.also {
// 写入完成后,通知所有依赖于此状态的组合“已失效,需要重组”
notifyWrite(snapshot, state)
}
}
// 找到当前 snapshot 下属于 state 的 StateRecord 节点(没有则新建),并返回
internal fun <T : StateRecord> T.overwritableRecord(
state: StateObject,
snapshot: Snapshot,
candidate: T
): T {
if (snapshot.readOnly) {
// 只读快照:不能直接写,只做变更登记
snapshot.recordModified(state)
}
val id = snapshot.id
// 如果 candidate 已经属于当前 snapshot,直接返回
if (candidate.snapshotId == id) return candidate
// 否则创建一个属于当前 snapshot 的新 StateRecord 节点
val newData = sync { newOverwritableRecordLocked(state) }
newData.snapshotId = id
snapshot.recordModified(state)
return newData
}
// 在每次变量更新的时候,Compose都会去查找哪些所有读过这个变量的位置,然后把这些位置标记为失效,在下一帧这些位置就会被重组,writeObserver干的就是这个事情。去查找这个变量在哪里被读了,然后把这部分的组合结果标记为失效,之后下一帧的时候,这些被标记的部分会进行重组。
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
1. readObserver 和 writeObserver 的区别和作用
-
readObserver:每当某个 StateObject(即 Compose 状态变量)被“读”时触发,用于记录“谁依赖了这个值”。
- 作用:注册依赖关系,让 Compose 知道“哪些地方要关注这个变量的变化”。
-
writeObserver:每当 StateObject 被“写”时触发,负责通知“依赖我的那些地方失效、需要重组”。
- 作用:触发 UI 重组。
注意:
这两者更多是“同一套依赖机制的不同环节”,不是完全“两个不同订阅系统”,只是触发场景和目的不同。
2. 两类订阅过程
第一类:Snapshot级别的依赖追踪(全局)
- 在 Snapshot 创建时,就注册好整个事务对哪些 StateObject 有“读/写依赖”。
- 这保证了 Compose 的快照隔离和多线程事务能力。
第二类:每个 StateObject 层级的依赖(具体到变量/组件)
- 在每次组合/读取某个 StateObject 时,都会注册依赖(readObserver)。
- 只要 StateObject 的值变化,就会根据依赖关系标记对应的组合为“失效”。
- 下一帧自动重组。
3. “只有发生在组合过程中的写事件才会执行 writeObserver”
-
组合内写 → 当次组合立刻标记失效并重组 → “马上”看到 UI 更新。
-
组合外写 → 本次组合不受影响 → 下次组合(通常是下一帧)才应用更新 → “延后”看到 UI 更新。
核心总结
- get = 注册依赖(订阅),set = 通知变化(触发 UI 重组)
- readObserver 负责注册依赖,writeObserver 负责通知依赖失效
- 依赖分为 Snapshot(全局事务)级和 StateObject(具体变量/组合)级
- 组合过程中的写才会触发完整的依赖追踪和 UI 重组,非组合过程的写不触发 UI 重组,但下次组合时仍会同步状态
setContent {
Box(modifier = Modifier.clickable {
// 只有发生在组合过程中的写事件才会去执行writeObsever,那么你可能担心,如果不通知,是否就会导致视图不会刷新呢?
name.value = "2" // 不会被writeObsever所见听到。snapshot.writeObserver?.invoke(state)不会触发。
}) {
Text(name.value)
name.value = "3" // 因为上边有Text(name.value),因此会触发writeObsever
}
}
上边的代码中,只要name.value的值发生变化, Text(name.value)就会重新走一遍,下边这种写法可以是因为有这两个方法:
val name by mutableStateOf("zjj")
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
// 下边两种方式的差异,value不需要了
var name by mutableStateOf("zjj")
setContent {
Box(modifier = Modifier.clickable {
name = "2"
}) {
Text(name)
name = "3"
}
}
val name = mutableStateOf("zjj")
setContent {
Box(modifier = Modifier.clickable {
name.value = "2"
}) {
Text(name.value)
name.value = "3"
}
}
简单总结
Compose 的状态订阅机制依赖于 MutableState 的 value 的 getter/setter 做了订阅(读)和通知(写)。底层链表支持快照和撤销。读写分别通过 readObserver/writeObserver 实现自动依赖追踪和 UI 更新。也就是说Compose 的状态订阅机制,就是自动追踪 UI 依赖的数据,只要数据变了就通知依赖的地方局部刷新。历史值链表+快照机制,让它能支持撤销、批量事务和多线程一致性。
remember —— 为什么必须用
| 场景 | 不用 remember | 用 remember { … } |
|---|---|---|
| 声明位置 | 在 setContent { … } 或其他 @Composable 函数体里直接写 var name by mutableStateOf("aaaa") | var name by remember { mutableStateOf("aaaa") } |
| 发生重组时 | 每一次重组都会重新执行那行代码 → 创建新的 MutableState 实例 → 把数值又设回 "aaaa" (旧实例仍被协程持有,但 UI 已不再使用它) | 只在第一次组合时初始化;之后返回缓存实例 |
| 结果 | 协程把旧实例改成 "dddd" UI 读的是新实例 → 界面不会变 | 同一个实例被协程和 UI 共用,写入后触发重组 → 界面正确更新 |
记忆要点
- 任何在组合代码块中创建的 可变对象/状态,除非包在
remember(或rememberSaveable等)里,否则都会在下一次组合重新创建。remember(key) { … }提供依赖列表:只要 key 引用发生变化就重新计算,无 key 则只初始化一次。
单一信息源 & 单向数据流
-
Single Source of Truth(SSOT) :状态放在 ViewModel / Repository 等外部层;UI 组件只读只写,不持有真数据。
-
Unidirectional Data Flow(UDF) :
- 上层状态下发到 UI (
state → UI) - UI 通过回调把意图上送 (
UI → event → ViewModel) - ViewModel 修改单一状态,再次下发 —— 始终“一个方向”循环
- 上层状态下发到 UI (
集合类型:必须用 Snapshot 集合
| 需求 | 正确做法 | 为什么 |
|---|---|---|
| 监听 列表 | val list = remember { mutableStateListOf(1,2,3) } | mutableStateOf(list) 只能监听 “引用变动”,不能感知 add/remove |
| 监听 Map | val map = remember { mutableStateMapOf(1 to "one") } | 同理,键值改动需要列表/映射版的 SnapshotState |
重组优化 & 结构性比较
-
Compose 先整体标记无效 → 执行函数 → 只有参数“真正变了”才往下递归重组。
-
“变没变”由 结构性相等 判断:默认调用
==(Kotlinequals)。 -
因此:
- 基础类型 /
String/ 全valdata-class → 可靠类型。 - 含
var的 data-class → 内部可能被改动但引用不变,Compose 无法可靠判断 → 会谨慎重组。
- 基础类型 /
-
若确实需要可变字段,可给类加
@Stable class User { var name by mutableStateOf("…") }编译器相信它能在字段变时通知 Compose,不再盲目重组。
组合期 VS 组合期外写状态
| 写入位置 | 是否立即触发 writeObserver | 何时刷新 UI |
|---|---|---|
| 组合内部(函数体里) | 是 | 当前帧 先标记失效 → 继续向下执行 → 立刻重组局部 |
| 组合外(点击回调、协程、定时器…) | 否 | 当前帧已结束,Compose 在下一帧统一调度一次重组 → UI 延后一帧更新 |
示例
setContent {
var name by remember { mutableStateOf("A") }
// 组合外写:点一下才执行
Box(Modifier.clickable { // ①
name = "B" // 当前帧不刷新,下一帧才刷新
}) {
Text(name) // ② 组合内写
name = "C" // ③ 立刻触发局部重组
}
}
- ③ 在组合体内,
writeObserver立即记下依赖(Text)并标记失效 → 当次组合结束前就把"C"绘到屏幕。 - ① 的写入发生在回调线程,当前组合已结束,
writeObserver不触发;Compose 在下一帧开始时发现状态已变,再重新组合,于是把"B"绘到屏幕。
提炼的使用准则
-
在组合代码里创建状态,一律
remember { … };需要跨进程/旋转保存时用rememberSaveable。 -
列表/Map:用
mutableStateListOf / mutableStateMapOf,否则无法追踪元素级变化。 -
高频参数尽量用可靠类型;若字段可变就:
- 用
mutableStateOf代理并加@Stable;或 - 把整个对象替换成新实例(不可变模型)。
- 用
-
事件回调里修改状态无需担心 UI 不刷 —— 只是 延后一帧。
-
思考重组范围:把耗时 UI 拆成单独 @Composable,并确保其参数是“可靠的”,即可让 Compose 自动跳过无效更新。
remember vs derivedStateOf —— 全面梳理 & 一眼就懂的示例
| 特性 | remember | derivedStateOf |
|---|---|---|
| 核心作用 | 把 任意对象/值 缓存到组合树中, 下一次重组直接复用,不再执行 lambda | 把 “某些 State 的计算结果” 包装成新的 State, 只有依赖的 State 发生变化才重新计算 |
| 是否追踪依赖 | 取决于 key: • 无 key → 永远只执行一次 • 有 key → 仅当 key 引用变动时再次执行 | 自动追踪 lambda 里读取的全部 State(值级别), 任何依赖值变化都会触发重算 |
| 返回值 | 直接返回 T(普通对象或 State) | 返回 State<R>,必须 .value 或 by 引用 |
| 典型用途 | • 缓存昂贵对象(动画、Painter、CoroutineScope…) • 记住第一次计算的常量/随机数 • 与 key 搭配,缓存轻量结果(如 String.uppercase) | • 列表过滤/排序、合并多 State 得到派生 State • 开关、按钮 Enable 状态等布尔派生 • 惰性统计(计数、平均值…) |
| 搭配方式 | 推荐 remember { derivedStateOf { … } } 形成“先缓存、再派生”的组合 | 单独用也行,但每次重组都会重新建对象, 失去缓存&依赖追踪优势 |
为什么需要它们配合?
val btnEnabled by remember { // ① 只创建一次
derivedStateOf { // ② 依赖追踪 + 缓存结果
text.value.isNotBlank() && !isSubmitting.value
}
}
remember负责“只创建一个派生 State 实例”——否则外层任何重组都会新建一个derivedStateOf,得不偿失。derivedStateOf内部再负责“只在真正依赖值改变时”重算 lambda,并把结果当作 State 分发。
常见坑 & 对策
| 现象 | 原因 | 对策 |
|---|---|---|
remember(list) { … } 无法在 list.add() 后更新 | MutableList 引用不变,remember 看不到变化 | 用 derivedStateOf { list.size };或换成 mutableStateListOf 并对其内容作依赖 |
在子 Composable 里 remember { name.uppercase() } ——父组件改了 name,子组件不刷新 | 传的是 普通 String,不是 State;remember 无 key,永远只算一次 | ① 给 key:remember(name) { … }; ② 或者把 name 本身作为 State 传入 |
用 derivedStateOf 但仍频繁执行 lambda | 忘了外层 remember { … },每次重组都会重建 | 把 derivedStateOf 包在 remember { … } 里 |
一眼就懂的完整示例
目标:输入框实时校验 + 过滤列表 + 动态按钮状态,演示 三种状态写法 的区别。
@Composable
fun UserSearchPage(allUsers: List<String>) {
/* -------- ① 原始输入状态 -------- */
var query by remember { mutableStateOf("") }
/* -------- ② 派生:过滤结果 -------- */
val filteredUsers by remember { // 只建一次
derivedStateOf { // 当且仅当 query 或 allUsers 变
if (query.isBlank()) allUsers
else allUsers.filter { it.contains(query, ignoreCase = true) }
}
}
/* -------- ③ 又一个派生:按钮是否可点 -------- */
val canSubmit by remember {
derivedStateOf { query.isNotBlank() && filteredUsers.isNotEmpty() }
}
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
/* 输入框:每击键必重组,但派生逻辑不会重复跑 */
OutlinedTextField(
value = query,
onValueChange = { query = it },
label = { Text("搜索用户") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
/* Submit 按钮只在 canSubmit 改变时重组 */
Button(
onClick = { /* do submit */ },
enabled = canSubmit, // 直接用派生 State
modifier = Modifier.align(Alignment.End)
) {
Text("提交")
}
Spacer(Modifier.height(16.dp))
/* 列表只在 filteredUsers 改变时重组 */
LazyColumn {
items(filteredUsers) { name ->
Text(name, Modifier.padding(vertical = 4.dp))
}
}
}
}
运行时能观察到:
- 击键 → 只重算一次过滤逻辑,按钮 & 列表精准刷新
- 其它父级布局重组(比如主题切换)→ 过滤/按钮状态都不会重新计算
- 代码简单、无手动缓存,也避免了耗时操作的重复触发
记住三条“顺口溜”
- 只记一次 →
remember { … } - 由谁而来 →
derivedStateOf { … } - 两者套娃 → “缓存派生”最稳妥
CompositonLocal 是状态但又不全是
CompositionLocal 本身不是 State,而是一个组合树的数据“通道”;只有当它承载的是 State 时,才具备“状态可变、自动追踪”的能力。如果 CompositionLocal 存放的是普通不可变对象,它只是“全局参数”,并不会自动触发重组。CompositionLocal 更像“环境变量”或“上下文”,本身不是“数据仓库”。CompositionLocal ≈ 数据通道(上下文),只有内容是 State 时才有“状态”的属性,本身并非 State。
CompositionLocal 是 Jetpack Compose 中提供的一种机制,具有穿透函数功能的局部变量,用于在 Compose 的组件树中共享数据,类似于传统 Android 开发中的依赖注入或全局上下文。它允许我们在组件树的任意位置提供数据,并在需要的地方读取这些数据,而不必通过参数层层传递。以下是 CompositionLocal 的主要概念和使用方法:
1. 定义 CompositionLocal
CompositionLocal 通常用来定义一些全局的状态,比如主题、字体或用户信息等。我们可以使用 staticCompositionLocalOf 或 compositionLocalOf 来定义一个 CompositionLocal 对象。
- compositionLocalOf → mutableStateOf(value) + 依赖链 + 快照链表
自动响应、自动通知、自动注册依赖,支持高阶场景,略重。 - staticCompositionLocalOf → 纯数据,没有响应式,没有依赖登记,没有快照链表
性能极高,只适合纯“全局只读配置”。
使用选择建议
- 只要传的值是基本类型/Color/不需要依赖注入的静态对象 → 用 staticCompositionLocalOf。
- 只要传的是依赖注入对象、ViewModel、或未来有可能会换引用 → 用 compositionLocalOf。 能 static 就 static,不要滥用 compositionLocalOfstatic 的情况下,IDE 编译也会优化一些调用链。
staticCompositionLocalOf 用于静态全局“配置型”数据,compositionLocalOf 用于“依赖注入”和引用动态切换场景。二者都不能追踪内容变动,能重组只是因为 provides 的“引用”换了。
mutableState 管理 = 记录/响应状态变化,依赖追踪 = 记录谁依赖我,快照 = 支持并发和批量状态历史。staticCompositionLocalOf 省略了这些“高阶特性”,所以极致轻量。
例如下边的示例中:
-
假设
BigScreen()内部有 1 000 个 Composable,只有 100 个真正用到颜色。 -
static 方案:改颜色 ⇒ 1 000 个节点都重组。
-
dynamic 方案:改颜色 ⇒ 只重组那 100 个节点,其余 900 个跳过。,
重组的原理底层还是用的 MutableState 实现的:
// 一个“静态” CompositionLocal,默认值如果没人提供就抛错
val StaticColor = staticCompositionLocalOf<Color> {
error("No StaticColor provided")
}
/* 同一个示例里还可能有: */
val DynamicColor = compositionLocalOf<Color> {
error("No DynamicColor provided")
}
// ⚠️ themeColor 会变
var themeColor by remember { mutableStateOf(Color.Red) }
// ① static
CompositionLocalProvider(StaticColor provides themeColor) {
BigScreen() // ← 整个重新执行
}
// ② dynamic
CompositionLocalProvider(DynamicColor provides themeColor) {
BigScreen() // 只重组真正调用 DynamicColor.current 的 composable
}
如果 LocalBackground 用的是 staticCompositionLocalOf,那么当CompositionLocalProvider(LocalBackground provides themeBackground) 的 themeBackground 发生变化时,这个 Provider 所包裹的整个代码块会“整体重组” (即它的全部子树都会重新执行)。这是因为:
-
staticCompositionLocalOf没有依赖追踪,必须全量通知staticCompositionLocalOf底层没有注册依赖,也没有 mutableState 的依赖追踪链。当 provides 的值发生变化(哪怕是新常量),Compose 没有办法只通知用到LocalBackground.current的地方——它只能选择“粗暴地把当前 Provider 下面的所有内容都重组一遍”。
-
compositionLocalOf的依赖追踪机制compositionLocalOf会为每个读取了LocalBackground.current的 Composable 节点在 SlotTable 注册依赖。 当 provides 的值变化时,只通知真正依赖了LocalBackground的那些节点重组,其余部分不会重组,提高了粒度和性能。
所以 CompositionLocalProvider(LocalBackground provides themeBackground) { ... } 这一整个 block 下的内容,全都被认为“可能依赖了 LocalBackground”,必须全重组,避免漏掉“隐式依赖” 。而用 compositionLocalOf 时,只有实际消费(LocalBackground.current)的下游 TextWithBackground() 发生重组,没用到的不会重组。
因此,虽然staticCompositionLocalOf没有记录的消耗了,但是提升了刷新的消耗。
val LocalExample = staticCompositionLocalOf<String> { error("No value provided") }
在这里,我们定义了一个名为 LocalExample 的 CompositionLocal,默认类型是 String,且没有默认值(没有值时会抛出异常)。
2. 提供值
为了让子组件能够访问 CompositionLocal 的值,我们需要在组件树中提供一个具体的值。可以通过 CompositionLocalProvider 来提供:
@Composable
fun ExampleProvider() {
CompositionLocalProvider(LocalExample provides "Hello, Jetpack Compose!" /*infix中缀函数,等价于LocalExample.provides("Hello, Jetpack Compose!")*/) {
ExampleConsumer()
}
}
在 CompositionLocalProvider 中,LocalExample 被赋值为 "Hello, Jetpack Compose!",这样在 ExampleConsumer 中就可以访问到这个值了。
3. 读取值
在组件中读取 CompositionLocal 的值非常简单,直接使用 .current 即可:
@Composable
fun ExampleConsumer() {
val exampleValue = LocalExample.current
Text(text = exampleValue) // 将显示 "Hello, Jetpack Compose!"
}
源码分析
// 这是 compositionLocalOf 的顶层函数
fun <T> compositionLocalOf(
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(), // 可以自定义变更策略(默认结构性等价)
defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)
// 动态型的 CompositionLocal,底层带 mutableState 的自动追踪
internal class DynamicProvidableCompositionLocal<T> constructor(
private val policy: SnapshotMutationPolicy<T>, // 状态变更策略
defaultFactory: () -> T
) : ProvidableCompositionLocal<T>(defaultFactory) {
@Composable
override fun provided(value: T): State<T> =
// 用 remember 包裹的 mutableStateOf,支持依赖追踪、快照、事务等
remember { mutableStateOf(value, policy) }.apply {
this.value = value // 赋值会触发 Compose 的响应式订阅
}
}
// 静态型的 CompositionLocal,只提供数据不追踪依赖
internal class StaticProvidableCompositionLocal<T>(defaultFactory: () -> T) :
ProvidableCompositionLocal<T>(defaultFactory) {
@Composable
override fun provided(value: T): State<T> =
StaticValueHolder(value) // 只是一个普通对象,直接包一层 State,不具备响应能力
}
// staticCompositionLocalOf 的顶层函数
fun <T> staticCompositionLocalOf(defaultFactory: () -> T): ProvidableCompositionLocal<T> =
StaticProvidableCompositionLocal(defaultFactory)
// 注意:没有 policy 参数!完全不支持自定义变更策略
// 简单的数据包装器,实现 State<T> 只是为了接口兼容
internal data class StaticValueHolder<T>(override val value: T) : State<T>
// 这里只存数据,没有 get/set,没有依赖登记,没有响应机制
重点总结:
-
compositionLocalOf → DynamicProvidableCompositionLocal
- 用
mutableStateOf(value, policy)包裹,支持依赖追踪、快照、事务。 - 有 policy 参数,可以自定义状态变化规则(比如按引用等价、内容等价)。
- 依赖于 Compose 响应式系统,任何依赖这个 CompositionLocal 的 Composable 都能被自动通知重组。
- 用
-
staticCompositionLocalOf → StaticProvidableCompositionLocal
- 只是
StaticValueHolder(value),仅作简单数据容器。 - 不关心 policy,也不支持依赖追踪。
- 只会全量通知下游重组,无响应式、无事务、无快照,性能更优但功能简单。
- 只是
学后测验
一、单选题(每题 2 分)
1. 矢量图不会失真的根本原因是?
A. 使用了高分辨率设备渲染
B. 基于数学公式、分辨率无关,可无限缩放
C. 将像素点预先缓存到内存
D. 通过 GPU 专用硬件加速
答案:B
解析:矢量图是由点、线、曲线等数学方程描述,不依赖固定像素网格,放大缩小时重新计算公式,因而无失真。
2. 下列哪个 Compose 组件最贴近 Android 原生的 FrameLayout?
A. Column
B. Box
C. Row
D. ConstraintLayout
答案:B
解析:Box 是最简单的单层布局,子元素可重叠,功能上等同于 FrameLayout。
二、多选题(每题 3 分,多选正确得满分)
3. 关于 Modifier 与组件参数的使用,下列说法正确的是?
A. 通用属性(尺寸/位置/外观/行为)应通过 Modifier 实现
B. 专用属性(文本溢出、按钮点击回调等)应通过函数参数提供
C. Modifier 的顺序对结果没有影响,只要链式调用即可
D. Modifier 的顺序会影响可点击区域的大小
E. clickable() 总是作用于整个组件(不受顺序影响)
答案:A, B, D
解析:
- A/B:官方设计理念:通用可组合的修饰器用
Modifier,专用功能用组件参数。 - D:
Modifier.clickable().padding()与padding().clickable()的点击区域不同,顺序决定响应尺寸。
4. 关于 compositionLocalOf 与 staticCompositionLocalOf,以下说法正确的有?
A. compositionLocalOf 底层包装了 mutableStateOf,支持依赖追踪
B. staticCompositionLocalOf 默认不会触发下游重组
C. staticCompositionLocalOf 在值变化时会全量重组其子树
D. staticCompositionLocalOf 接受自定义 SnapshotMutationPolicy 参数
E. compositionLocalOf 会接受并应用变更策略(policy)
答案:A, C, E
解析:
- A/E:
compositionLocalOf用remember { mutableStateOf(value, policy) },可自定义 policy 并进行细粒度依赖追踪。 - C:
staticCompositionLocalOf不做依赖登记,只能在 Provider 值变时粗暴重组整个子树。 - B/D:B 错,它会触发重组;D 错,它不接收 policy 参数。
三、判断题(每题 1 分,判断正确打“√”,错误打“×”)
5. val name = remember { mutableStateOf("x") } 每次重组都会重新初始化为 "x"。
- 答案:×
- 解析:
remember保证初次组合后缓存对象,后续重组不会再执行初始化。
6. 如果不使用 remember 包裹,直接写 derivedStateOf { ... },每次重组都会重新创建派生状态。
- 答案:√
- 解析:
derivedStateOf本身只是定义依赖,若不再remember中缓存,每次重组都会重新实例化。
四、简答题(每题 5 分)
7. 简述三种状态声明方式的区别:
val a = mutableStateOf("zjj")
val b by mutableStateOf("zjj")
val c by remember { mutableStateOf("zjj") }
答案要点:
a = mutableStateOf:访问需写a.value,且每次重组都会重新执行赋初值,不持久。b by mutableStateOf:用 Kotlin 委托省略.value,但每次重组同样会重置。c by remember { mutableStateOf }:首次初始化后缓存,不会因重组重置,推荐持久化状态。
8. 描述 Compose 中可变状态的“三层结构”(MutableState、StateObject、StateRecord)及其目的,并简要说明 @Stable 注解的作用。
答案要点:
-
三层结构:
MutableState<T>:API 层,供用户读写.value。StateObject:维护一个StateRecord链表头,管理所有历史快照。StateRecord:链表节点,存储某次快照(snapshotId)下的值。
-
目的: 支持多快照事务、批量更新、回滚、并发隔离,以及精准依赖追踪。
-
@Stable: 告诉 Compose 某个类型在满足条件下“引用不变即内容不变”,可安全用于跳过等价性检查,提高重组性能。
五、编程题(每题 10 分)
9. 编写一个简单的 Composable,需求如下:
- 使用
mutableStateListOf保存一组字符串(初始["aa","bb"])。 - 通过
remember { derivedStateOf { /*…*/ } }派生大写列表。 - 点击按钮
"Add"时,向列表追加"cc"。 - 将派生后的大写列表通过
LazyColumn展示。
@Composable
fun UppercaseListDemo() {
// TODO: 按题意补全
}
参考答案
@Composable
fun UppercaseListDemo() {
// 1. 原始列表
val items = remember { mutableStateListOf("aa", "bb") }
// 2. 派生大写列表,记得用 remember 包裹 derivedStateOf
val upperItems by remember {
derivedStateOf { items.map { it.uppercase() } }
}
Column {
Button(onClick = { items.add("cc") }, Modifier.padding(8.dp)) {
Text("Add")
}
LazyColumn {
items(upperItems) { str ->
Text(str, Modifier.padding(4.dp))
}
}
}
}
解析:
- 用
mutableStateListOf使列表内部增删可观察; derivedStateOf自动跟踪items内容变化;remember保证只创建一次派生状态对象;LazyColumn高效渲染动态列表。
10. 编写一个 Composable,展示动态和静态 CompositionLocal 的使用。
- 定义一个
compositionLocalOf<String>(动态)和一个staticCompositionLocalOf<String>(静态)。 - 在不同
CompositionLocalProvider中分别提供不同值,并在内部用Text同时输出两者的.current。
// 定义 CompositionLocal
val LocalDynamic = compositionLocalOf { "DynDef" }
val LocalStatic = staticCompositionLocalOf { "StaDef" }
@Composable
fun CompositionLocalDemo() {
// TODO: 补全 Provider 和消费代码
}
参考答案
@Composable
fun CompositionLocalDemo() {
// 第一层:提供动态 & 静态默认值
CompositionLocalProvider(
LocalDynamic provides "HelloDyn",
LocalStatic provides "HelloSta"
) {
// 下层消费
Box(Modifier.padding(8.dp)) {
Text("D=${LocalDynamic.current} / S=${LocalStatic.current}")
}
}
// 再次提供动态新值、静态值不变
CompositionLocalProvider(LocalDynamic provides "NewDyn") {
Box(Modifier.padding(8.dp)) {
Text("D=${LocalDynamic.current} / S=${LocalStatic.current}")
}
}
}
解析:
compositionLocalOf包裹mutableStateOf,动态跟踪依赖,仅重新组合真正消费LocalDynamic.current的节点;staticCompositionLocalOf不做依赖追踪,若变更只能全量重组整个 Provider 子树;- 示例中两次输出可观察到动态值切换,而静态值只有在对应 Provider 中变化时才会影响下游。