Jetpack Compose 四大核心概念 —— @Composable、Modifier、State、Recomposition

本文深入剖析 Compose 的四个核心概念,帮你从「会用」进阶到「理解」。配合 Compose 从 0 到 1 阅读效果最佳。

@Composable —— UI 就是函数

作用

@Composable 是 Compose 的灵魂注解。被它标记的函数就是一个 UI 组件,可以直接在别的函数里调用。

核心规则

规则说明
函数名首字母大写约定俗成,与普通函数区分(如 MyButton 而非 myButton
只能被 @Composable 函数调用普通函数不能直接调用 Composable 函数
没有返回值Composable 函数通常返回 Unit(描述 UI,不返回对象)
可接收任意参数通过参数控制 UI 外观和行为,实现复用
默认没有顺序调用顺序不代表实际排列顺序(需要在布局容器中才有顺序)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个 Composable 控件
@Composable
fun MyTitle() {
Text(text = "我是标题")
}

// 在别的地方直接调用函数名就能用
@Composable
fun MyPage() {
MyTitle()
MyTitle()
MyTitle() // 调三次就显示三个标题
}

带参数的可复用组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Composable
fun GreetingCard(name: String, isVip: Boolean = false) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.background(
if (isVip) Color(0xFFFFD700) else Color.White,
RoundedCornerShape(8.dp)
)
.padding(12.dp)
) {
Text(
text = "欢迎,$name",
fontWeight = if (isVip) FontWeight.Bold else FontWeight.Normal
)
if (isVip) {
Text("👑", modifier = Modifier.padding(start = 8.dp))
}
}
}

// 使用
@Composable
fun UserList() {
Column {
GreetingCard("张三")
GreetingCard("李四", isVip = true)
GreetingCard("王五")
}
}

适用场景

  • 一切 UI 相关的函数:控件、布局、页面、弹窗、标题栏等

⚠️ 注意

  • Composable 函数可能被频繁调用(重组时),不要在内部做耗时操作
  • 副作用(网络请求、数据库读写)应使用 LaunchedEffect / SideEffect
  • 函数内部创建的局部变量,每次重组都会重新创建
  • 命名规范:Composable 函数首字母大写,参数顺序按重要性排列

Modifier —— 所有外观全靠它

作用

Modifier 是 Compose 的外观装饰系统,控制控件的尺寸、内边距、背景、圆角、边框、点击、滚动等一切外观和行为。所有控件都可以通过 modifier 参数接收 Modifier。

核心概念

链式调用

1
2
3
4
5
Modifier
.size(100.dp) // 最外层:固定 100x100
.background(Blue) // 第二层:蓝色背景
.padding(16.dp) // 第三层:内边距 16dp
.clip(CircleShape) // 最内层:圆形裁剪

Modifier 是从外到内依次包裹的。上面这段代码的效果:最外限定 100x100 区域 → 铺蓝底 → 在蓝色内部再留 16dp 边距 → 最终裁剪成圆形。

顺序决定效果

1
2
3
4
5
6
7
8
9
// 先 background 再 padding:背景色填满,然后内容区域向内缩
Modifier
.background(Color.Red)
.padding(16.dp)

// 先 padding 再 background:内边距也被背景色覆盖
Modifier
.padding(16.dp)
.background(Color.Red)
顺序效果
.background(...).padding(...)背景在 padding 之外,背景区域更大
.padding(...).background(...)背景在 padding 之内,背景区域更小

常用 Modifier 分类速查

尺寸类

Modifier说明
size(width, height)固定宽高
width(dp) / height(dp)固定宽度 / 高度
fillMaxWidth() / fillMaxHeight()填满父容器宽度 / 高度
fillMaxSize()宽高都填满
wrapContentWidth() / wrapContentHeight()包裹内容宽度 / 高度
defaultMinSize(minWidth, minHeight)最小尺寸约束
sizeIn(minWidth, maxWidth, minHeight, maxHeight)尺寸范围约束

内边距类

Modifier说明
padding(all)四边相同
padding(horizontal, vertical)水平和垂直
padding(start, top, end, bottom)分别设置

外观类

Modifier说明
background(color)纯色背景
background(color, shape)带形状的背景
clip(shape)裁剪形状
border(width, color)边框
border(width, color, shape)带形状的边框
shadow(elevation, shape)阴影
alpha(fraction)透明度
rotate(degrees)旋转
scale(scaleX, scaleY)缩放
offset(x, y)偏移(不影响布局)

交互类

Modifier说明
clickable { }点击
combinedClickable(onClick, onLongClick)点击 + 长按

布局类

Modifier说明
weight(fraction)权重分配(Row / Column 内)
align(alignment)对齐(Box 内)

综合示例

1
2
3
4
5
6
7
8
9
10
11
Box(
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(12.dp))
.background(Color.Blue)
.border(2.dp, Color.White, RoundedCornerShape(12.dp))
.clickable { },
contentAlignment = Alignment.Center
) {
Text("点我", color = Color.White)
}

适用场景

  • 几乎所有控件的外观控制:设置大小、加边距、上颜色、加边框、添加交互

⚠️ 注意

  • 顺序很重要:从外到内依次应用,不同顺序效果不同
  • 同一个 Modifier 只能使用一次:不能写两个 .padding(),需要用参数组合
  • 自定义 Modifier:通过扩展函数封装复用逻辑
  • 可组合Modifier 是不可变的,每个 .xxx() 返回一个新的 Modifier 对象

状态(State)—— 数据变了,界面自动刷新

作用

State 是 Compose 最核心的心智模型:你只管改数据,UI 自动刷新。不需要 findViewByIdsetTextnotifyDataSetChanged

核心 API

mutableStateOf —— 创建可观察变量

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

Column {
Text(text = "当前计数:$count")
Button(onClick = { count++ }) {
Text("点我 +1")
}
}
}
关键字作用
mutableStateOf(0)创建一个”可观察”的变量,值变了会自动通知界面刷新
remember { }让变量在界面重组时”记住”当前值,不会被重置
byKotlin 委托语法,让你直接写 count++ 而不是 count.value++

mutableStateOf 的三种写法

1
2
3
4
5
6
7
8
9
10
11
// 方式 1:委托(推荐,最简洁)
var count by remember { mutableStateOf(0) }
count++ // 直接用

// 方式 2:直接使用 .value
val count = remember { mutableStateOf(0) }
count.value++ // 通过 .value 访问

// 方式 3:解构声明
val (value, setValue) = remember { mutableStateOf(0) }
setValue(value + 1)

常见状态类型

状态写法示例
基本类型mutableStateOf(0)计数器、开关状态
字符串mutableStateOf("")输入框内容
布尔值mutableStateOf(false)展开/折叠、选中状态
列表mutableStateListOf<T>()待办列表、聊天记录
集合mutableStateMapOf<K, V>()键值对状态
对象mutableStateOf(MyData())表单数据
可空类型mutableStateOf<T?>(null)可选数据

列表状态示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Composable
fun TodoList() {
val todoList = remember { mutableStateListOf<String>() }
var inputText by remember { mutableStateOf("") }

Column {
Row {
TextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f)
)
Button(onClick = {
if (inputText.isNotBlank()) {
todoList.add(inputText)
inputText = ""
}
}) {
Text("添加")
}
}
LazyColumn {
items(todoList.size) { index ->
Text(
text = "${index + 1}. ${todoList[index]}",
modifier = Modifier.padding(8.dp)
)
}
// 列表变化 → 自动刷新!
}
}
}

remember 的进阶用法

remember(key) —— 依赖 key 变化重新计算

1
2
3
4
5
6
7
8
@Composable
fun UserProfile(userId: String) {
// 当 userId 变化时,重新加载用户数据
val userData = remember(userId) {
loadUserData(userId) // 仅 userId 变化时执行
}
Text("用户名:${userData.name}")
}

rememberSaveable —— 屏幕旋转后保留

1
2
3
4
5
// remember:屏幕旋转后丢失
var temp by remember { mutableStateOf("") }

// rememberSaveable:屏幕旋转后保留(自动存到 Bundle)
var saved by rememberSaveable { mutableStateOf("") }

remember 生命周期

1
2
3
4
5
6
7
8
9
10
首次进入 Composable
remember {} 执行,创建状态

用户交互,状态变化
→ 触发重组(Recomposition)
remember 返回已有值,不重新创建

从界面移除(不再显示)
→ 状态被销毁
→ 下次显示时重新创建

适用场景

  • 任何需要「数据变化 → UI 自动更新」的场景:表单、列表、动画、主题切换等

⚠️ 注意

  • 必须用 remember 包裹var count = mutableStateOf(0)(缺少 remember)每次重组都会重置
  • 状态提升(State Hoisting):将状态提升到父组件,通过参数传递,实现单向数据流
  • mutableStateListOfmutableStateMapOf 自带可观察能力,内部元素变化会自动刷新
  • 避免过度使用:不是所有变量都需要 state,只有 UI 相关的才需要

重组(Recomposition)—— 谁变了就刷新谁

作用

当状态(State)变化时,Compose 会智能地只重绘受影响的控件,没有变化的部分原地不动。这是 Compose 高性能的核心机制。

工作原理

1
2
3
4
5
6
7
8
9
State 变化

Compose 标记「受影响的 Composable 函数」

执行 Recomposition:重新调用被标记的函数

对比新旧 UI,仅更新变化的部分

不受影响的函数 → 直接跳过,零开销

直观理解

1
2
3
count 变了 → 只有 Text("当前计数:$count") 会重新渲染
→ Button 不动
→ 页面上 100 个其他控件也不动

重组的特点

特性说明
选择性只重组读取了变化状态的函数
乐观任何状态变化都可能触发重组,但 Compose 会跳过无变化的部分
可中断新的状态变化可以打断正在进行的重组
并行多个 Composable 可以并行执行重组
幂等同一输入多次重组结果相同(不应该有副作用)

避免不必要的重组

使用不可变数据

1
2
3
4
5
// ❌ 不稳定,每次重组都算新对象
data class User(var name: String, var age: Int)

// ✅ 稳定,值不变对象就不变
data class User(val name: String, val age: Int)

使用 derivedStateOf 派生状态

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun FilteredList(items: List<String>, query: String) {
// ❌ 每次重组都重新过滤(即使 query 没变)
val filtered = items.filter { it.contains(query) }

// ✅ 仅在 items 或 query 变化时重新过滤
val filtered by remember { derivedStateOf { items.filter { it.contains(query) } } }

LazyColumn {
items(filtered) { item -> Text(item) }
}
}

3. 状态读取延迟

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun HeavyItem(onClick: () -> Unit, isSelected: Boolean) {
// ⚠️ isSelected 变化 → 整个 HeavyItem 重组
// 包括 1000 行的复杂 UI
Box(modifier = Modifier.clickable(onClick = onClick)) {
// ... 1000 行复杂 UI
if (isSelected) {
Border()
}
}
}
1
2
3
4
5
6
7
8
@Composable
fun HeavyItem(onClick: () -> Unit, isSelected: Boolean) {
// ✅ 把 isSelected 读解放到单独的 Composable 中
Box(modifier = Modifier.clickable(onClick = onClick)) {
// ... 1000 行复杂 UI(不重绘)
SelectionBorder(isSelected) // 仅这个函数被重绘
}
}

SideEffect —— 在重组中安全执行副作用

1
2
3
4
5
6
7
8
9
10
@Composable
fun AnalyticsTracker(pageName: String) {
// ❌ 不要在 Composable 直接做副作用(会在每次重组时执行)
// FirebaseAnalytics.logEvent("page_view", ...)

// ✅ 使用 LaunchedEffect,仅 pageName 变化时执行
LaunchedEffect(pageName) {
logPageView(pageName)
}
}
副作用 API使用场景
LaunchedEffect(key)协程操作,key 变化时重新启动
SideEffect每次重组后执行(同步)
DisposableEffect(key)需要清理的副作用(如注册/注销监听)
rememberCoroutineScope()获取 Composable 生命周期内的协程作用域

适用场景

  • 理解 Compose 性能、排查 UI 不刷新 bug、优化复杂列表

⚠️ 注意

  • Composable 函数应该幂等:同样的输入,无论调用多少次结果相同
  • 不要在 Composable 中直接做副作用:使用 Effect API
  • 不要依赖 Composable 函数的调用顺序和次数:Compose 可能会跳过、重复调用
  • 不要读取或修改全局变量:使用 State 代替
  • 使用 Layout Inspector 或 Compose 编译器报告的「restartable」「skippable」标记来排查性能问题