3.1 什么是可组合函数
可组合函数是带有 @Composable 注解的 Kotlin 函数,它是 Compose 构建 UI 的最小单元。
kotlin
@Composable
fun TextMessage(text: String) {
Text(text = text)
}
但这段看似普通的函数背后,蕴藏着大量编译器魔法。
3.2 @Composable 注解的编译器魔法
编译器插件的插入
当你给函数加上 @Composable 注解,Kotlin Compose 编译器插件会做三件事:
① 添加额外的参数
编译器会为每个 Composable 函数隐式添加 Composer 和 Applier 参数:
kotlin
// 你的代码
@Composable
fun TextMessage(text: String) {
Text(text = text)
}
// 编译器转换后的等效代码(概念简化)
fun TextMessage(text: String, $composer: Composer, $applier: Applier<Any?>) {
// 1. 开始组合作用域
$composer.startRestartGroup()
// 2. 状态追踪
$composer.track(text)
// 3. 调用子 Composable
Text(text = text, composer = $composer, applier = $applier)
// 4. 结束作用域
$composer.endRestartGroup()
}
② 插入重组代码
每次状态变化时,Compose 运行时通过 Composer 对象跟踪哪些 Composable 函数读取了哪些状态。当状态改变时,只重新执行对应的函数。
③ 生成位置信息
编译器为每个组合单元分配 groupKey 和位置信息,用于 Compose 在重组时准确定位和复用。
@Composable 的传染性
kotlin
@Composable
fun Outer() {
Inner() // ✅ OK,在 Composable 上下文中
}
@Composable
fun Inner() {
Text("Hello")
}
fun NonComposable() {
// Text("Hello") ❌ 编译错误!
// 不能在非 Composable 函数中调用 Composable 函数
}
@Composable 是传染性的------被它标记的函数只能在同一上下文中被调用。这意味着你的 UI 代码天然地被限制在正确的组合作用域内。
3.3 Composable 函数的三大规则
规则一:无副作用(Side Effect Free)
Composable 函数可能因重组被频繁调用,因此它必须幂等------相同的输入产生相同的输出。
kotlin
// ❌ 违反:Composable 中直接执行副作用
@Composable
fun BadCounter() {
val count = mutableStateOf(0)
// 每次重组都会创建新的协程!
CoroutineScope(Dispatchers.IO).launch {
saveToDatabase(count.value)
}
Text("Count: ${count.value}")
}
// ✅ 正确:使用 LaunchedEffect 管理副作用
@Composable
fun GoodCounter() {
var count by remember { mutableStateOf(0) }
// 仅在首次组合时执行
LaunchedEffect(Unit) {
saveToDatabase(count)
}
Text("Count: $count")
}
规则二:不依赖外部条件执行
kotlin
// ❌ 违反:依赖函数调用顺序
var globalFlag = false
@Composable
fun A() {
if (globalFlag) Text("A") // 外部条件不可控
}
@Composable
fun B() {
globalFlag = true
Text("B")
}
Composable 函数的执行顺序不应影响结果,否则重组时会出现难以追踪的 bug。
规则三:重组应尽可能快速
Composable 函数可能因动画或高频状态变化而被频繁调用。不要在 Composable 中进行:
kotlin
// ❌ 避免:阻塞性操作
@Composable
fun SlowComponent(data: List<Item>) {
// 每次重组都进行大量排序计算
val sorted = data.sortedBy { it.complexCalculation() }
LazyColumn { items(sorted) { Item(it) } }
}
// ✅ 优化:用 derivedStateOf 或 remember 缓存
@Composable
fun FastComponent(data: List<Item>) {
val sorted by remember(data) {
derivedStateOf { data.sortedBy { it.complexCalculation() } }
}
LazyColumn { items(sorted) { Item(it) } }
}
3.4 Composable 函数命名规范与最佳实践
命名规范
- 名词大写开头 :
UserProfile、MessageList、LoadingIndicator - 动词短语加 by 后缀 :
Button而不是DisplayButton - 有语义,无歧义:宁长勿短
参数惯例
kotlin
// 惯例 1:modifier 作为第一个可选参数
@Composable
fun UserAvatar(
url: String,
modifier: Modifier = Modifier, // 第一个可选参数
size: Int = 48,
onClick: (() -> Unit)? = null
)
// 惯例 2:内容 lambda 作为最后一个参数
@Composable
fun Card(
modifier: Modifier = Modifier,
content: @Composable () -> Unit // 最后一个参数
)
这是遵循 Material Design 组件的设计惯例,让组件调用方式统一。
单一职责
kotlin
// ❌ 一个 Composable 包含太多逻辑
@Composable
fun ComplexScreen() {
var text by remember { mutableStateOf("") }
var items by remember { mutableStateOf(listOf<Item>()) }
var isLoading by remember { mutableStateOf(false) }
Column {
// 搜索栏
TextField(value = text, onValueChange = { text = it })
// 列表
if (isLoading) CircularProgressIndicator()
else LazyColumn { items(items) { ... } }
// 底部按钮
Button(onClick = {}) { Text("Submit") }
}
}
// ✅ 拆分为多个小函数
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
var state by remember { mutableStateOf<UiState>(Loading) }
Column {
SearchBar(query = query, onQueryChange = { query = it })
when (val s = state) {
is Loading -> LoadingIndicator()
is Success -> ResultList(s.items)
is Error -> ErrorView(s.message)
}
SubmitButton()
}
}
原则:一个 Composable 只做一件事。既是代码组织的良好实践,也是重组优化的关键------更小的函数意味着更精确的重组范围。
3.5 @Preview 进阶用法
kotlin
// 基础预览
@Preview(showBackground = true)
@Composable
fun SimplePreview() { ... }
// 多设备预览
@Preview(
showBackground = true,
showSystemUi = true,
device = "id:pixel_6_pro"
)
@Preview(
showBackground = true,
device = "id:pixel_tablet"
)
@Composable
fun MultiDevicePreview() { ... }
// 深色模式
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
@Composable
fun DarkPreview() { ... }
// 字体缩放
@Preview(fontScale = 1.5f)
@Composable
fun LargeFontPreview() { ... }
// 带参数预览 ------ 用 @PreviewParameter
class MessageParameterProvider : PreviewParameterProvider<Message> {
override val values = sequenceOf(
Message("Hello", "John"),
Message("Long text...", "Alice")
)
}
@Preview
@Composable
fun MessagePreview(
@PreviewParameter(MessageParameterProvider::class) msg: Message
) {
MessageCard(msg)
}
多预览技巧 :对同一个 Composable 添加多个 @Preview 注解(如上例的多设备),Android Studio Preview 面板会并排显示所有配置的渲染效果。
3.6 方法引用与函数式思想
在 Compose 中,Composable 函数是一等公民。你可以像传递数据一样传递 UI 函数:
kotlin
// 将 Composable 作为参数传递
@Composable
fun ScreenLayout(
header: @Composable () -> Unit,
content: @Composable () -> Unit,
footer: @Composable () -> Unit = {}
) {
Column {
Box(modifier = Modifier.height(56.dp)) { header() }
Box(modifier = Modifier.weight(1f)) { content() }
footer()
}
}
// 使用时
ScreenLayout(
header = { TopAppBar(title = { Text("Home") }) },
content = { Greeting("Android") },
footer = { BottomNavigationBar() }
)
这种函数式的组合方式,使 UI 复用达到前所未有的灵活度------不需要继承、不需要接口回调。
3.7 本章练习
- 写一个
UserCardComposable,包含头像、昵称、简介三个展示元素 - 为
UserCard添加多设备 Preview(手机 + 平板 + 深色模式) - 创建一个
ProfileScreen,组合SearchBar+UserCard+PostList三个子 Composable - 尝试将 Composable 函数作为参数传递,实现一个通用的
LoadingWrapper组件
kotlin
// 💡 LoadingWrapper 示例
@Composable
fun <T> LoadingWrapper(
state: UiState<T>,
onRetry: () -> Unit,
content: @Composable (data: T) -> Unit
) {
when (state) {
is Loading -> CircularProgressIndicator()
is Success -> content(state.data)
is Error -> ErrorView(state.message, onRetry)
}
}
3.8 本章小结
| 内容 | 要点 |
|---|---|
| 本质 | 带 @Composable 注解的 Kotlin 函数,编译器为之生成重组追踪代码 |
| 三大规则 | 无副作用、不依赖外部顺序、快速执行 |
| 命名规范 | 名词大写开头、modifier 惯例、content lambda 惯例 |
| 单一职责 | 一个 Composable 做一件事,利于复用和优化 |
| Preview | 多设备、深色模式、字体缩放、PreviewParameter 多种姿势 |
| 函数式复用 | Composable 可作为参数传递,实现高阶 UI 模式 |
下一篇:布局系统总览------从 Row、Column 到 Box 的声明式布局思维。