Jetpack Compose 入门

529 阅读36分钟

Jetpack Compose 初了解

Compose 独立于平台的真实含义是:它的上层完全不依赖 Android,但底层的实现还是需要 Android 的 API 去做。

矢量图(Vector Graphics)只是描述规则,不需要将真实像素表达出来,特点是占用空间小、不怕放大。矢量图不会失真,原因如下:

矢量图为什么不会失真?

  1. 基于数学公式:矢量图使用点、线、曲线和多边形等基本几何元素,由数学方程精确定义。例如,直线用两个点的坐标和方向描述,曲线用贝塞尔曲线公式表示。
  2. 分辨率独立:由于是基于数学定义,矢量图与设备分辨率无关。放大或缩小时,软件会重新计算方程,图像质量不会受影响。
  3. 无像素限制:矢量图不存在固定像素网格,放大时不会出现像素化(锯齿状边缘)。位图则会因为像素拉伸而失去清晰度。
  4. 可无限缩放:矢量图可以无限放大或缩小,不影响清晰度和细节,适合用于 LOGO、字体、插图等设计。

Compose 组件 API 与安卓原生的对应关系

传统 View 系统Jetpack Compose说明/补充
TextViewText()完全对应,Compose 的 Text() 更灵活
ImageViewImage()对应,Compose 可直接用 Image 渲染图片
FrameLayoutBox()对应,Box 是单层布局,可以重叠放置子元素
LinearLayoutColumn() / Row()对应,Column 纵向,Row 横向布局
RelativeLayoutBox() (仅作简单重叠)不完全等价,Compose 的 Box 只支持简单重叠
ConstraintLayoutConstraintLayout()Compose 也有 ConstraintLayout,需要加依赖
MotionLayoutMotionLayout()Compose 支持 MotionLayout(Accompanist/官方支持)
ScrollViewColumn(Modifier.verticalScroll())横向用 Row + Modifier.horizontalScroll()
RecyclerViewLazyColumn() / LazyRow()对应,Compose 懒加载组件,效率高
ViewPager2Pager()Pager 在 Accompanist/Compose Foundation

具体说明

  1. RelativeLayout ≠ Box

    • Box 只支持“简单的重叠”,不支持 RelativeLayout 那种“依赖兄弟 View 位置” 的复杂关系。
    • 如果你要类似“左对齐、右对齐、某个 View 在另一个下方”这种,建议用 ConstraintLayout for Compose(需要依赖:implementation "androidx.constraintlayout:constraintlayout-compose:...")。
  2. ConstraintLayout、MotionLayout

    • Compose 原生就有 ConstraintLayout,API 比传统略有不同,但能力对等甚至更强。
    • MotionLayout 目前 Compose 也支持,但还没像 View 那样完善,大多数场景已经够用。
  3. ScrollView → Column/Row + Scroll 修饰符

    • Compose 没有“ScrollView”组件,而是给 Column/Row 添加 Modifier.verticalScroll()Modifier.horizontalScroll() 实现可滚动。
  4. RecyclerView → LazyColumn/LazyRow

    • Compose 的 LazyColumn/LazyRow 专门为高效长列表设计,是 RecyclerView 的直接替代。
  5. ViewPager → Pager

    • Pager 是 Compose 的分页组件,最初在 Accompanist,现在已经集成到 Compose Foundation 中(androidx.compose.foundation.pager),用法类似 ViewPager2。

小结

  • 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 把指令变为屏幕能识别的二维像素数据。

image.png

每一个UI都会被转化成 LayoutNode,轻量级的东西,而View或者ViewGroup非常重,这样做的目的是为了做复用和拓展。

image.png

大量能力“外包”出去,LayoutNode 可以动态组合属性:

  • 添加 text 属性就是 Text
  • 添加 image 属性就是 Image
  • 添加 button 属性就是 Button

所有属性都转化为 Modifier,如 paddingModifier
AndroidComposeView 相当于“创世主”,是 Compose 世界的起点。

image.png

Compose的分层

分层与依赖:上层依赖下层

material(3) 一堆material设计风格组件的包,button、TextField等。floating action button,这些组件都具有一定的固定风格,类似于app的一种主题,在某种主题下,所有的空间都具有某种一致性。

foundation 相对可以用的UI体系,column row、BasicTextFiled等

animation  动画层

ui   ui最基础的支持,测量,布局,绘制,等。

runtime compose最底层的机制,数据结构,转化机制等

-·-·-·-·-·-·-·-·-·-·

compile 编译层

Compose的包依赖原则

  1. 写代码的时候,依赖material(3)就够了,可能跳过material 依赖 foundation就够了。
  2. 如果需要ui-tooling,预览功能,需要单独把它写出来;
  3. 如果需要 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 = 101102103110 ...         │
│                                               │
│  - 记录了哪些 StateObject 被读/写             │
│  - 记录了哪些 Composable 需要重组             │
│  - 支持事务、撤销、合并                       │
└───────────────────────────────────────────────┘

为什么要设计三层结构(MutableState → StateObject → StateRecord)?

  1. 一般变量只存当前值就够了,为什么 Compose 这么复杂?
    因为 Compose 不只是要保存“最新值”,还要支持“事务”功能,比如:

    • 批量更新多个状态后一次性提交(类似数据库的事务)
    • 支持撤销、回滚(回到某个历史状态)
  2. 为什么需要保存“历史值”?
    只有存下历史值,才能让事务或撤销成为可能。例如,用户操作时如果需要撤回操作,框架就能恢复到某个历史快照。

如何实现对历史状态的管理?

  1. 用什么方式存多个历史值?

    • Compose 用链表结构来保存每个变量在不同时刻的值(每次变更就新增一个节点)。
    • 这个链表的头节点叫 firstStateRecord,每个节点就是一个 StateRecord,带有快照 id 和当时的值。
  2. 三层结构的意义:

    • 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. 多层状态对象
  • MutableStateStateObjectStateRecord
  • 目的是支持事务(批量更新、撤销、并发多快照等)——所以状态不仅有当前值,还有历史值(链表结构)。
2. 读写的订阅与通知
  • get(读取)时: 注册订阅(readObserver),记录“谁用了我”。
  • set(写入)时: 通知所有依赖我的地方(writeObserver),触发重组/刷新。

两种订阅机制

  1. 对 Snapshot(快照)的读写订阅:

    • readObserver: 每次 get 都会调用,注册依赖。
    • writeObserver: 每次 set 都会调用,通知相关 UI 更新。
  2. 对每个 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

关键说明:

  1. MutableState → StateObject → StateRecord(链表)

    • MutableState 的 getter 调用 readable(),底层进入 StateObjectStateRecord 链表,读取带有当前 snapshotId 的节点。
  2. readObserver / writeObserver

    • readObserverget() 时被 Snapshot 调用,用来向 SlotTable 注册“哪个 Composable(组ID)读取了我”。
    • writeObserverset() 时被 Snapshot 调用,用来通知 SlotTable 把依赖我的那批组ID标记失效。
  3. 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)            │
│           (仅重组了依赖该状态的区块)               │
└────────────────────────────────────────────────────────┘
  1. MutableState → StateObject → StateRecord(链表)

    • MutableState 是对外暴露的可变状态。
    • StateObject 管理一个 StateRecord 链表,记录每个快照下的值历史。
    • 每个 StateRecord 带有一个 snapshotId,关联到某个 Snapshot
  2. Snapshot / readObserver / writeObserver

    • readObserver:每次在 Composable 中读取状态(get())时,由当前 Snapshot 调用,向 StateObject 注册「当前正在执行的组 ID」。
    • writeObserver:每次写入状态(set())时,由 Snapshot 调用,标记所有依赖过该状态的组 ID「失效」。
  3. SlotTable & 组合

    • SlotTable 维护整个组合树中各个 Composable 调用的「组 ID」与代码位置映射。
    • 当某个组 ID 被 writeObserver 标记失效后,Compose 会通过 SlotTable 精确定位到对应的 Composable 段落,只重组这一部分,而非整棵树。

这样,Compose 就能做到:状态改变只通知真正依赖的 UI 片段,且重组时快速跳过未变化的部分,既保证了响应式正确性,又实现了高效更新。

  1. 链表的意义:每个变量的历史状态用 StateRecord 链表保存,每个节点有独立的 snapshotId,实现多事务并发、批量回滚等功能。
  2. 依赖通知核心:所有依赖于这个状态的 UI 组合,被 writeObserver 标记为“失效”,下一帧会重组,实现响应式数据流。
  3. policy:决定新旧值是否“等价”。只有变更时才写入,否则不触发重组,优化性能。
  4. 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. readObserverwriteObserver 的区别和作用

  • 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 —— 为什么必须用

场景不用 rememberremember { … }
声明位置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)

    1. 上层状态下发到 UI (state → UI)
    2. UI 通过回调把意图上送 (UI → event → ViewModel)
    3. ViewModel 修改单一状态,再次下发 —— 始终“一个方向”循环

集合类型:必须用 Snapshot 集合

需求正确做法为什么
监听 列表val list = remember { mutableStateListOf(1,2,3) }mutableStateOf(list) 只能监听 “引用变动”,不能感知 add/remove
监听 Mapval map = remember { mutableStateMapOf(1 to "one") }同理,键值改动需要列表/映射版的 SnapshotState

重组优化 & 结构性比较

  1. Compose 先整体标记无效 → 执行函数 → 只有参数“真正变了”才往下递归重组。

  2. “变没变”由 结构性相等 判断:默认调用 ==(Kotlin equals)。

  3. 因此:

    • 基础类型 / String / 全 val data-class → 可靠类型
    • var 的 data-class → 内部可能被改动但引用不变,Compose 无法可靠判断 → 会谨慎重组。
  4. 若确实需要可变字段,可给类加

    @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" 绘到屏幕。

提炼的使用准则

  1. 在组合代码里创建状态,一律 remember { … } ;需要跨进程/旋转保存时用 rememberSaveable

  2. 列表/Map:用 mutableStateListOf / mutableStateMapOf,否则无法追踪元素级变化。

  3. 高频参数尽量用可靠类型;若字段可变就:

    • mutableStateOf 代理并加 @Stable;或
    • 把整个对象替换成新实例(不可变模型)。
  4. 事件回调里修改状态无需担心 UI 不刷 —— 只是 延后一帧

  5. 思考重组范围:把耗时 UI 拆成单独 @Composable,并确保其参数是“可靠的”,即可让 Compose 自动跳过无效更新。

remember vs derivedStateOf —— 全面梳理 & 一眼就懂的示例

特性rememberderivedStateOf
核心作用任意对象/值 缓存到组合树中, 下一次重组直接复用,不再执行 lambda“某些 State 的计算结果” 包装成新的 State, 只有依赖的 State 发生变化才重新计算
是否追踪依赖取决于 key: • 无 key → 永远只执行一次 • 有 key → 仅当 key 引用变动时再次执行自动追踪 lambda 里读取的全部 State(值级别), 任何依赖值变化都会触发重算
返回值直接返回 T(普通对象或 State)返回 State<R>,必须 .valueby 引用
典型用途• 缓存昂贵对象(动画、Painter、CoroutineScope…) • 记住第一次计算的常量/随机数 • 与 key 搭配,缓存轻量结果(如 String.uppercase)• 列表过滤/排序、合并多 State 得到派生 State • 开关、按钮 Enable 状态等布尔派生 • 惰性统计(计数、平均值…)
搭配方式推荐 remember { derivedStateOf { … } } 形成“先缓存、再派生”的组合单独用也行,但每次重组都会重新建对象, 失去缓存&依赖追踪优势

为什么需要它们配合?

val btnEnabled by remember {          // ① 只创建一次
    derivedStateOf {                  // ② 依赖追踪 + 缓存结果
        text.value.isNotBlank() && !isSubmitting.value
    }
}
  1. remember 负责“只创建一个派生 State 实例”——否则外层任何重组都会新建一个 derivedStateOf,得不偿失。
  2. 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))
            }
        }
    }
}

运行时能观察到:

  • 击键 → 只重算一次过滤逻辑,按钮 & 列表精准刷新
  • 其它父级布局重组(比如主题切换)→ 过滤/按钮状态都不会重新计算
  • 代码简单、无手动缓存,也避免了耗时操作的重复触发

记住三条“顺口溜”

  1. 只记一次 → remember { … }
  2. 由谁而来 → derivedStateOf { … }
  3. 两者套娃 → “缓存派生”最稳妥

CompositonLocal 是状态但又不全是

CompositionLocal 本身不是 State,而是一个组合树的数据“通道”;只有当它承载的是 State 时,才具备“状态可变、自动追踪”的能力。如果 CompositionLocal 存放的是普通不可变对象,它只是“全局参数”,并不会自动触发重组。CompositionLocal 更像“环境变量”或“上下文”,本身不是“数据仓库”。CompositionLocal ≈ 数据通道(上下文),只有内容是 State 时才有“状态”的属性,本身并非 State。

CompositionLocal 是 Jetpack Compose 中提供的一种机制,具有穿透函数功能的局部变量,用于在 Compose 的组件树中共享数据,类似于传统 Android 开发中的依赖注入或全局上下文。它允许我们在组件树的任意位置提供数据,并在需要的地方读取这些数据,而不必通过参数层层传递。以下是 CompositionLocal 的主要概念和使用方法:

1. 定义 CompositionLocal

CompositionLocal 通常用来定义一些全局的状态,比如主题、字体或用户信息等。我们可以使用 staticCompositionLocalOfcompositionLocalOf 来定义一个 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") }

在这里,我们定义了一个名为 LocalExampleCompositionLocal,默认类型是 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. 关于 compositionLocalOfstaticCompositionLocalOf,以下说法正确的有?
A. compositionLocalOf 底层包装了 mutableStateOf,支持依赖追踪
B. staticCompositionLocalOf 默认不会触发下游重组
C. staticCompositionLocalOf 在值变化时会全量重组其子树
D. staticCompositionLocalOf 接受自定义 SnapshotMutationPolicy 参数
E. compositionLocalOf 会接受并应用变更策略(policy)

答案:A, C, E
解析:

  • A/E:compositionLocalOfremember { 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") }

答案要点:

  1. a = mutableStateOf:访问需写 a.value,且每次重组都会重新执行赋初值,不持久。
  2. b by mutableStateOf:用 Kotlin 委托省略 .value,但每次重组同样会重置。
  3. c by remember { mutableStateOf }:首次初始化后缓存,不会因重组重置,推荐持久化状态。

8. 描述 Compose 中可变状态的“三层结构”(MutableStateStateObjectStateRecord)及其目的,并简要说明 @Stable 注解的作用。

答案要点:

  • 三层结构:

    1. MutableState<T>:API 层,供用户读写 .value
    2. StateObject:维护一个 StateRecord 链表头,管理所有历史快照。
    3. 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 中变化时才会影响下游。