4. 可组合函数与布局基础:Column、Row、Box
说实话,刚接触 Compose 的时候,我最大的困惑就是:一个函数怎么就能画 UI 了? 传统的 XML 写惯了,突然让我用 Kotlin 函数描述界面,总觉得心里没底。但用了一段时间后,我反而觉得这种方式更符合直觉。
这一章我们就把 Compose 最核心的三个布局摸透:Column、Row、Box。它们是搭界面的「三块积木」,几乎所有页面都能用它们拼出来。
4.1 可组合函数:UI 就是函数
先看一段最基础的代码:
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
这个 @Composable 注解告诉编译器:这个函数是用来生成 UI 的。我在早期项目里经常犯一个错误——以为可组合函数里只能放 UI 代码。其实你可以在里面写任何 Kotlin 逻辑,比如循环、判断,甚至调用网络请求(虽然不推荐在 UI 层直接做网络调用)。
核心理解:可组合函数不是「画」UI,而是描述 UI 状态。当状态变化了,Compose 会智能地重新执行这个函数,只更新变化的部分。
嗯,这里要注意:可组合函数不应该有副作用。比如在里面写 SharedPreferences 的写入操作,就会导致每次重组都写一次,性能直接崩掉。我之前有个同事就踩过这个坑,日志里全是重复写入记录……
4.2 Column:纵向排列,像流水线
Column 是最常用的布局之一。它把子元素从上到下排成一列,类似传统 Android 的 LinearLayout 垂直模式。
@Composable
fun ColumnExample() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("第一行", fontSize = 20.sp)
Text("第二行", fontSize = 16.sp)
Button(onClick = { /* 点击事件 */ }) {
Text("按钮")
}
}
}
我个人习惯在 Column 里总是显式指定 horizontalAlignment,哪怕默认是 Start。为什么?因为后期你可能随时调整对齐方式,写着名能省去很多 debug 时间。
避坑指南:我曾经在 Column 里嵌套了一个 LazyColumn,结果一运行就报错「Vertically scrollable component ...」。这是因为 Column 无法测量无限高度的子元素。解决办法是用 Modifier.weight() 限制高度,或者改用 Box 配合 ScrollState。
4.3 Row:横着干活,并排显示
Row 和 Column 对称,只是排列方向变成了水平。我最常用它来做顶部导航栏和列表项里的图标+文字组合。
@Composable
fun RowExample() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "搜索"
)
Spacer(modifier = Modifier.width(8.dp))
Text("搜索框占位", color = Color.Gray)
}
}
你看,Row 里用 Spacer 控制间距,比传统 margin 语义更清晰。不过要注意:Spacer 本身不占视觉空间,只占布局空间,千万别往它上面设背景色,看不到的。
| 布局 | 排列方向 | 常用场景 | 易犯错误 |
|---|---|---|---|
| Column | 垂直 | 列表、表单、纵向信息流 | 嵌套可滚动组件 |
| Row | 水平 | 导航栏、图标+文字、并排按钮 | 宽度溢出屏幕 |
| Box | 层叠 | 叠放、遮罩、装饰元素 | 子元素尺寸未指定 |
4.4 Box:堆叠的艺术
Box 允许子元素在 Z 轴上重叠。我第一次用 Box 是在做一个「头像+在线状态绿点」的组件。想想看,用传统 XML 你得用 FrameLayout 或 RelativeLayout,而 Compose 里一个 Box 就搞定了:
@Composable
fun AvatarWithBadge() {
Box(
modifier = Modifier.size(64.dp)
) {
// 背景头像
Image(
painter = painterResource(id = R.drawable.avatar),
contentDescription = "头像",
modifier = Modifier.fillMaxSize()
)
// 右上角薄点
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(16.dp)
.background(Color.Green, shape = CircleShape)
)
}
}
注意 align 修饰符——它决定了子元素在 Box 内部的相对位置。Box 默认所有子元素都从左上角开始堆叠,但用 Modifier.align(Alignment.Center) 就能调整。我记得有次写界面,忘了加 align,结果所有文字都挤在左上角,调试了半天才发现。
警告:Box 的子元素如果没有明确尺寸,可能会以 0 尺寸显示。比如上面那个绿点,如果没写 .size(16.dp),它就是一个不可见的点。你想想看,找 bug 得多辛苦?
4.5 修饰符:布局的灵魂
没有修饰符(Modifier)的布局就像没有调料的菜——能吃但没味道。Column、Row、Box 都需要配合 Modifier 控制尺寸、边距、对齐、权重等。
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color(0xFFF5F5F5))
) {
Text("内容", modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp))
}
这里有一个顺序问题:Modifier 链的顺序会影响最终效果。比如 .padding().background() 和 .background().padding() 完全不同:前者是在内边距内填背景,后者是背景从外边开始。我在项目里见过因为顺序反了,导致背景色范围比预期大出一圈的情况。
4.6 权重:像 flex 一样分配空间
Row 和 Column 都支持 Modifier.weight(),用来按比例分配剩余空间。这比传统 Android 的 layout_weight 更简洁,因为它直接在子元素上声明:
Row(modifier = Modifier.fillMaxWidth()) {
Text("左", modifier = Modifier.weight(1f))
Text("中", modifier = Modifier.weight(2f))
Text("右", modifier = Modifier.weight(1f))
}
上面这段代码会让「中」占据两倍于「左」和「右」的空间。我个人建议在需要等分布局时,直接给每个子元素 weight(1f),简单粗暴。不过注意:weight 只在未指定固定宽度的子元素上生效,如果某个子元素写了 .width(100.dp),那它就不会参与权重分配。
4.7 组合使用:一个真实案例
我们来做一个「商品卡片」的布局,综合运用 Column、Row、Box:
@Composable
fun ProductCard(productName: String, price: String, imageUrl: String) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = 4.dp
) {
Row(modifier = Modifier.padding(12.dp)) {
// 商品图片(用 Box 包一层,方便后期加角标)
Box(modifier = Modifier.size(80.dp)) {
Image(painter = painterResource(id = R.drawable.product), contentDescription = null)
// 折扣角标
Text(
text = "5折",
color = Color.White,
fontSize = 12.sp,
modifier = Modifier
.align(Alignment.TopStart)
.background(Color.Red, shape = RoundedCornerShape(4.dp))
.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
// 右侧文字信息
Column(modifier = Modifier.weight(1f)) {
Text(text = productName, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = price, color = Color(0xFFE91E63))
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { /* 加入购物车 */ }) {
Text("加入购物车")
}
}
}
}
}
你看,一个复杂卡片就由 Row、Column、Box 嵌套组合而成。真做项目时,我通常会先把布局结构在脑子里画一遍,确定每层用什么容器,再写代码——这样不容易乱。
小技巧:开发时可以在每个容器上临时加 .border(1.dp, Color.Red) 来可视化布局边界,排查完再删掉。我在团队里推过这个习惯,新手 debug 效率至少提升 30%。
4.8 本章知识结构
下面这张图总结了 Column、Row、Box 的核心关系和应用场景:
其实说白了,Column、Row、Box 对应三维空间的三个方向:Y、X、Z。把这三个方向掌握好,再复杂的界面都能拆解成它们的组合。我刚开始带新人时,就让他们用这三种布局自己画一个「设置页」,效果比直接给代码好得多。