3、Compose基础入门:@Composable注解、声明式UI模型、Modifier基础、布局基础
好,我们直接进入正题。这一章咱们把Compose的骨架搭起来。你可能看过一些教程,上来就讲各种组件怎么用。但我个人的习惯是,先搞懂底层那几条核心规则。规则明白了,后面怎么写都顺手。
Compose和传统的View系统,说白了是两套完全不同的思想。你如果带着旧思路来学,会很别扭。我们先看最根本的东西——@Composable。
3.1 @Composable:你写的每一个“组件”都是函数
在Compose里,没有类继承,没有LayoutInflater,没有findViewById。你写界面,就是在写函数。
核心认知:每一个带@Composable注解的函数,都是一个UI单元。它会告诉编译器:“这个函数能转化为界面的一部分。”
举个例子:
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
就这么简单。Greeting函数本身没有任何特殊之处,除了那个@Composable注解。这个注解就像是给函数贴了个标签,Compose的编译器看到它,就会去生成管理UI状态的底层代码。
我在项目中遇到过同事问:“为什么我的Composable函数不能从普通函数里调用?”
嗯,这个问题很典型。原因是@Composable函数只能在另一个@Composable函数的作用域里被调用。它依赖一个特殊的“Composition上下文”,普通函数没有这个上下文。你想想看,如果能在任意地方调用UI函数,那状态管理和重组就乱套了。
警告:不要试着在@Composable函数里执行耗时操作或挂起函数。Composable函数可能被频繁调用(重组),任何阻塞操作都会导致UI卡顿。
3.2 声明式UI模型:你描述目标,框架负责实现
传统View系统是“命令式”的:你告诉系统每一步怎么做——先创建TextView,找到它,再setText。Compose是“声明式”的:你直接描述界面“应该长什么样”。
为什么这样更好?
因为“描述”天然就是幂等的。你传什么数据,界面就渲染成什么样。数据变了,Compose会“智能地”重新执行那些受影响的Composable函数,这叫重组(Recomposition)。
我曾经为一个复杂的多状态页面调试过很久。旧代码里到处都是if-else判断可见性、更新文本。后来用Compose重写,一个when表达式把三种状态描述清楚,问题全消失了。说白了,你把精力花在定义“是什么”,而不是“怎么做”。
技巧:刚开始写Compose时,容易不自觉地去想“怎么更新这个TextView”。忘记它。你只需要想:我的数据是什么?界面应该根据这些数据长什么样子?剩下的交给Compose。
3.3 Modifier基础:布局和装饰的“万能工具包”
Modifier是Compose里最常用的东西。你可以把它想象成一个构建器,用来给组件添加尺寸、边距、背景、点击事件等等。
关键点:Modifier是链式调用的,顺序很重要。
@Composable
fun ModifierDemo() {
Box(
modifier = Modifier
.size(100.dp) // 先设尺寸
.padding(16.dp) // 再加上内边距(这会缩小可用区域)
.background(Color.Blue) // 最后给剩余区域上色
)
}
我个人的习惯是,把size或fillMaxSize这类“布局约束”放在最前面,然后才是padding、background这些“装饰”。
为什么顺序重要?
因为Modifier链的执行是从外到内的。一个常见的坑是:
// 错误顺序
Box(
modifier = Modifier
.background(Color.Red)
.padding(16.dp)
)
// 实际效果:先画红色背景,然后在红色区域里切出16dp的内边距区域
我曾经就因为顺序搞反,调了半天UI对不齐。你一定记着:background、border这类“绘制”操作,尽量放在padding、size之后,否则它们会被覆盖掉。
常用Modifier速查表:
| Modifier | 作用 | 示例 |
|---|---|---|
size() | 固定宽高 | .size(48.dp) |
fillMaxSize() | 尽可能填满父容器 | .fillMaxSize(0.5f)(一半父容器) |
padding() | 内边距 | .padding(horizontal = 16.dp) |
background() | 填充背景色 | .background(Color.Blue, shape = RoundedCornerShape(8.dp)) |
clickable() | 添加点击交互 | .clickable { /* 处理点击 */ } |
border() | 添加边框 | .border(1.dp, Color.Gray) |
3.4 布局基础:Column、Row、Box
有了@Composable和Modifier,我们还得把组件合理地摆放在屏幕上。Compose提供了三个最基础的布局容器,几乎能组合出任何界面。
3.4.1 Column:纵向排列
Column让你的子组件从上到下依次排列。可以指定垂直对齐方式(Arrangement)和水平对齐方式(Alignment)。
@Composable
fun ColumnDemo() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceEvenly, // 子项均匀分布
horizontalAlignment = Alignment.CenterHorizontally // 水平居中
) {
Text("第一行")
Text("第二行")
Button(onClick = { /* ... */ }) {
Text("点击")
}
}
}
3.4.2 Row:横向排列
Row和Column刚好相反,子组件从左到右排列。用法完全对称。
@Composable
fun RowDemo() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Favorite, contentDescription = null)
Text("标题")
Switch(checked = true, onCheckedChange = {})
}
}
3.4.3 Box:层叠布局
Box最大的特点是子组件可以重叠。默认从左上角开始堆叠,通过Modifier.align()可以精确控制每个子组件的位置。
@Composable
fun BoxDemo() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
) {
// 第一个子组件:铺满整个Box
Text(
text = "背景文字",
modifier = Modifier.align(Alignment.Center)
)
// 第二个子组件:放在右上角
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
)
}
}
我在做一个浮层提示时,就是用Box实现的:底层放内容,上层悬浮一个半透明提示框,用Align.BottomCenter定位。Box真的是处理“层叠”场景的利器。
实践建议:界面布局90%用Column和Row就能搞定。只有需要子组件重叠、或者定位时,才考虑用Box。不要滥用。
好了,到这里,我们已经把Compose最核心的四块拼图都讲清楚了。下面这张图可以帮你理清它们的关系:
你看,整个Compose的界面搭建逻辑其实很简单:用@Composable声明函数,函数内部用Column、Row、Box来摆放内容,每个元素通过Modifier链调整外观和交互。这就是声明式的力量——你不再操纵具体的View对象,而是通过函数组合来描述界面。
下一章,我们会进入状态管理。但在此之前,我建议你亲自动手写一个简单的界面:比如一个用户卡片,包含头像、姓名、简介,用Column和Row组合出来。试几次,你就找到感觉了。
易犯错误:学习初期容易把所有布局都塞进一个Column里。遇到复杂布局时,记得拆解成多个小的@Composable函数,每个函数只负责一个逻辑块。组合优于继承,这在Compose里是铁律。