Android Compose学习02:基础组件与 Modifier

目录

前言:从"命令式"到"声明式"

[一、@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 是必需参数,这是和 XML android: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(...) })------这条先记着,性能阶段展开。

相关推荐
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(102):UMA——用多个未来问题端到端训练一份共享记忆
论文阅读·人工智能·学习·开源·github
xxwl5852 小时前
第六章---计算几何
笔记·学习·算法
minglie12 小时前
Visual Studio问题
学习
其实防守也摸鱼2 小时前
存储管理:页面置换算法学习任务(测验题要求给出置换页面顺序,缺页次数,缺页率)
android·前端·数据库·学习·oracle·自动化
dadaobusi2 小时前
学习: gdb自动捕获调用栈
学习
Song_RsHt2 小时前
Gateway (上):路由与转发,微服务的统一入口
java·笔记·后端·学习·gateway
2603_969579282 小时前
备份云盘是什么意思?简单说就是“用云盘来备份数据”
学习
mmsx3 小时前
Android 地图上绘制 KML:解析、样式与渲染
android·xml
运行时异常3 小时前
【无标题】
android