目录
[一、@Composable 函数](#一、@Composable 函数)
[1.1. 基本规则](#1.1. 基本规则)
[1.2. 三个重要性质](#1.2. 三个重要性质)
[1.3. 数据流向:单向数据流](#1.3. 数据流向:单向数据流)
[二、@Preview 预览](#二、@Preview 预览)
[2.1. 基本用法](#2.1. 基本用法)
[2.2. 常用参数](#2.2. 常用参数)
[2.3. 一个函数叠多个预览](#2.3. 一个函数叠多个预览)
[2.4. 预览需要数据?用 PreviewParameter](#2.4. 预览需要数据?用 PreviewParameter)
[三、Composable 的"生命周期"](#三、Composable 的"生命周期")
[3.1. 关键理解](#3.1. 关键理解)
[3.2. 由此推出三条铁律](#3.2. 由此推出三条铁律)
[3. 渲染的三个阶段](#3. 渲染的三个阶段)
[4.1. Text](#4.1. Text)
[4.2. Button 家族](#4.2. Button 家族)
[4.3. Image](#4.3. Image)
[4.4. TextField](#4.4. TextField)
[4.5. Card](#4.5. Card)
[4.6. Switch](#4.6. Switch)
[五、Modifier 系统](#五、Modifier 系统)
[5.1. 是什么](#5.1. 是什么)
[5.2. 顺序敏感](#5.2. 顺序敏感)
[5.3. 常用 Modifier 速查](#5.3. 常用 Modifier 速查)
[5.4. 两个纪律](#5.4. 两个纪律)
前言:从"命令式"到"声明式"
XML + findViewById 的世界里,UI 是一棵对象树 ,你拿到引用去"改"它(textView.text = "...")。
Compose 的世界里,UI 是函数的输出:
Kotlin
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
-
你不持有 View 引用,只描述"给定状态,UI 应该长什么样";
-
状态变了,框架重新执行函数(这叫重组 Recomposition),自动算出差异并更新屏幕;
-
所以 Compose 代码里几乎不出现"设置/更新 UI"的动词,只有"声明"。
记住一句话:UI = f(state)。阶段 1 的所有内容都是在学怎么写这个 f。
一、@Composable 函数
1.1. 基本规则
Kotlin
@Composable
fun UserCard(user: User, modifier: Modifier = Modifier) {
Card(modifier = modifier) {
Column(modifier = Modifier.padding(16.dp)) {
Text(user.name, style = MaterialTheme.typography.titleMedium)
Text(user.bio, style = MaterialTheme.typography.bodySmall)
}
}
}
-
注解
@Composable告诉编译器这个函数会被编译成"能往界面上发射 UI 节点"的代码; -
返回类型是 Unit,它不"返回"一个 View,而是在执行时把 UI 挂到界面上;
-
命名约定:大驼峰、名词 (
UserCard、SearchBar),因为它代表一个界面元素,不是一个动作; -
只能在另一个 @Composable 的上下文里调用 Composable 函数------这保证了组合树的层级。
1.2. 三个重要性质
-
可重启(Restartable):状态变化时框架可以从任意一个 Composable 开始重新执行,而不是整棵树;
-
可跳过(Skippable):如果参数没变(且类型是 stable 的),重组时直接跳过这个函数------这是 Compose 性能的核心机制;
-
幂等 :同样的参数应该输出同样的 UI,所以不要在 Composable 里做副作用(比如发网络请求、写文件)------副作用要用
LaunchedEffect等 API;
1.3. 数据流向:单向数据流
参数(状态)向下流动 ,事件(lambda)向上传递:
Kotlin
@Composable
fun SearchBar(
query: String, // 状态下来
onQueryChange: (String) -> Unit, // 事件上去
modifier: Modifier = Modifier
) {
TextField(value = query, onValueChange = onQueryChange, modifier = modifier)
}
组件自己不藏状态,谁用谁持有------这叫状态提升(State Hoisting),你现在先建立直觉,阶段 2 学状态时会系统讲。
二、@Preview 预览
@Preview 替代了以前 XML 的 tools: 命名空间 + 布局预览,而且强得多------你可以对任意小组件单独预览,不用打开整个页面
2.1. 基本用法
Kotlin
@Preview(showBackground = true)
@Composable
private fun UserCardPreview() {
MyTheme {
UserCard(User("张三", "Android 开发者"))
}
}
注意:Preview 函数必须无参 (或用 PreviewParameter),且建议 private;记得包一层你自己的 Theme,否则颜色字体不是应用里的样子。
2.2. 常用参数
Kotlin
@Preview(
name = "深色模式", // 预览面板里的名字
showBackground = true, // 加白底
showSystemUi = true, // 连状态栏/导航栏一起渲染(整屏效果)
device = Devices.PIXEL_7, // 指定设备规格
uiMode = Configuration.UI_MODE_NIGHT_YES, // 深色模式
fontScale = 1.3f, // 模拟系统大字体
locale = "en" // 模拟其他语言
)
2.3. 一个函数叠多个预览
Kotlin
@Preview(name = "浅色", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "深色", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun UserCardVariants() { /* ... */ }
一个 Composable 生成多张预览图,做深浅色、字体缩放、多语言适配自查极其方便。
2.4. 预览需要数据?用 PreviewParameter
Kotlin
class UserProvider : PreviewParameterProvider<User> {
override val values = sequenceOf(
User("张三", "短简介"),
User("一个特别特别长的名字", "一段特别长的简介,用来验证换行和省略号......")
)
}
@Preview
@Composable
private fun UserCardPreview(@PreviewParameter(UserProvider::class) user: User) {
MyTheme { UserCard(user) }
}
每个值生成一张预览------边界数据(超长文本、空值)都能提前暴露。这直接对应 XML 时代用 tools:text 假装数据的做法,但能批量跑。
三、Composable 的"生命周期"
严格说 Composable 不是 View,没有 onCreate/onDestroy。它的生命周期是在组合(Composition)
进入组合(初次执行) → 重组(执行 0 次到 N 次) → 离开组合(不再显示)
3.1. 关键理解
-
进入组合:函数第一次执行,UI 节点被创建;
-
重组 :函数里读到的状态变了 → 框架标记这个函数需要重跑 → 智能地只重跑受影响的部分。重组可能非常频繁(动画每帧一次),也可能一次都没有;
-
离开组合 :条件分支不再满足(
if (show) { A() }中 show 变 false),节点被移除。
3.2. 由此推出三条铁律
① 不要在组合里放"只应该执行一次"的代码。
Kotlin
@Composable
fun Bad() {
val list = mutableListOf<String>() // ❌ 每次重组都新建一个
// ...
}
要用 remember 把对象"存"在组合里:
Kotlin
val list = remember { mutableStateListOf<String>() } // ✅ 重组间保留
不要在组合里做副作用。 比如直接在函数体里 Toast.makeText(...).show()------重组一次弹一次。副作用进 LaunchedEffect、DisposableEffect(阶段 2 细学)。
③ UI 结构由条件控制,而不是 visibility。
Kotlin
// XML 思维:View.visibility = View.GONE
// Compose 思维:不组合它
if (isLoading) {
CircularProgressIndicator()
} else {
Content(data)
}
3. 渲染的三个阶段
任何一次屏幕更新都走 Composition(组合)→ Layout(布局:测量+摆放)→ Drawing(绘制) 。为什么要现在知道?因为很多性能优化的本质是"让变化发生在越靠后的阶段越好"(比如用 offset {} lambda 版,让移动只触发 Layout 而不触发重组)------阶段 1 你只需建立这个框架感。
四、基础组件介绍
先有一个全局认识:Compose 组件库默认是 Material 3 (androidx.compose.material3),所有组件自动带主题色、形状、字体;另外它们普遍采用 Slot API ------很多参数是 @Composable () -> Unit 类型的"插槽",你往里塞任意内容,比 View 体系的自定义属性灵活得多。
4.1. Text
Kotlin
Text(
text = "标题",
color = MaterialTheme.colorScheme.primary,
fontSize = 20.sp, // 字号用 sp
fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis, // 超长省略号
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge, // 推荐:直接用主题样式,别散装堆参数
)
要点:优先用 style 引用主题的 typography ,散装参数只用于局部覆盖。多个样式拼接用 buildAnnotatedString(类似以前的 SpannableString)。
4.2. Button 家族
Kotlin
Button(
onClick = { /* 点击逻辑 */ },
enabled = isFormValid,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF00696E)),
shape = RoundedCornerShape(12.dp),
) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("添加") // content 是 RowScope 插槽,横向排列
}
-
变体:
Button(实心)、OutlinedButton(描边)、TextButton(纯文字)、IconButton、FloatingActionButton------对应 Material 的按钮层级; -
onClick是必需参数,这是和 XMLandroid:onClick/setOnClickListener的本质区别:按钮天生可点,不用你再绑监听器。
4.3. Image
Kotlin
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "用户头像", // 无障碍!纯装饰图传 null
modifier = Modifier
.size(64.dp)
.clip(CircleShape), // 圆形裁剪,一行搞定
contentScale = ContentScale.Crop // 对应 ImageView 的 scaleType
)
要点:contentDescription 是一等公民参数 ,不是事后补的 accessibility 属性------这是官方刻意设计。网络图用 Coil 的 AsyncImage。
4.4. TextField
Kotlin
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it }, // ← 必须自己把新值写回状态!
label = { Text("用户名") },
placeholder = { Text("请输入") },
singleLine = true,
isError = text.length > 20,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
最大的坑 :EditText 自己持有文本,而 TextField 不持有状态 。如果你写了 value = "" 而 onValueChange 里不把新值存回去,输入框会永远打不上字。记住"value + onValueChange 成对出现,状态提升给调用方"这个模式,它就是 Compose 输入控件的灵魂(Switch、Slider 同理)。
TextField 是 filled 样式,OutlinedTextField 是描边样式,底层还有个裸的 BasicTextField 给你完全自定义。
4.5. Card
Kotlin
Card(
onClick = { /* Material3 的 Card 自带可点击版本 */ },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
modifier = Modifier.fillMaxWidth()
) {
Column(Modifier.padding(16.dp)) { /* 内容插槽,随意塞 */ }
}
对应以前的 CardView,但内容、点击、颜色全部通过参数和插槽注入。
4.6. Switch
Kotlin
var checked by remember { mutableStateOf(false) }
Switch(
checked = checked,
onCheckedChange = { checked = it }, // 又是"状态下来、事件上去"
thumbContent = if (checked) {
{ Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(SwitchDefaults.IconSize)) }
} else null
)
同族还有 Checkbox、RadioButton、Slider,全部遵循同一个受控组件模式。
五、Modifier 系统
5.1. 是什么
Modifier 是对组件的装饰/行为修饰链,每个元素包裹下一个,像洋葱:
Kotlin
Text(
"Hello",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(Color(0xFFE3F2FD), RoundedCornerShape(8.dp))
.clickable { /* ... */ }
.padding(12.dp)
)
约定俗成:每个自定义组件都接收 modifier: Modifier = Modifier 作为第一个可选参数 ,并把它应用到最外层节点------你上面 UserCard 的例子就是这么写的。
5.2. 顺序敏感
链上每个元素把测量约束和绘制效果传给下一个,所以交换顺序 = 不同结果。
案例 A:padding 与 background
Kotlin
// ① padding 在前:先撑开16dp间距,背景只涂内容区
Modifier.padding(16.dp).background(Color.Blue)
// 效果:蓝色块,四周露出父级背景
// ② background 在前:先涂满整个区域,再向内缩16dp放内容
Modifier.background(Color.Blue).padding(16.dp)
// 效果:蓝色块更大,文字与蓝边有16dp内边距
案例 B:padding 与 clickable
Kotlin
// ① 热区包含 padding,波纹更大
Modifier.clickable { }.padding(16.dp)
// ② 热区只有内容本身,padding 区域点了没反应
Modifier.padding(16.dp).clickable { }
案例 C:size 与 padding
Kotlin
Modifier.padding(10.dp).size(100.dp) // 总占 120dp(padding 在 size 外)
Modifier.size(100.dp).padding(10.dp) // 总占 100dp,内容被挤到 80dp
案例 D:offset 与 padding
padding 会参与布局,把兄弟元素顶开;
offset 只做绘制时的位移,不占位、不影响别人(适合做动画位移、悬浮微移);
offset 是 RTL 感知的,absoluteOffset 不感知。
5.3. 常用 Modifier 速查
| XML 属性 | Compose Modifier |
| layout_width="match_parent" | fillMaxWidth() / fillMaxSize() |
| wrap_content | 默认行为,不用写 |
| layout_margin | padding(...) 放前面(对外) |
| padding | padding(...) 放后面(对内) |
| gravity / layout_gravity | Row/Column 的 Arrangement/Alignment,或 Box 里 Modifier.align() |
| background | background(color, shape) |
| onClick | clickable { } / combinedClickable { }(带长按) |
| visibility="gone" | 不写 Modifier,用 if 不组合它 |
| clipToOutline + 圆角 | clip(RoundedCornerShape(...)) |
| alpha | alpha(0.5f) |
weight |
Row/Column 作用域内 的 Modifier.weight(1f) |
|---|
5.4. 两个纪律
-
链式调用中每个元素只写一次,不能"叠加" :写两个
padding是两个洋葱层,不是相加; -
性能提示 :Modifier 链在重组时如果内容没变会被复用,但尽量把不变的链提出去或用
remember,动画类参数优先用 lambda 版(如offset { IntOffset(...) })------这条先记着,性能阶段展开。