D3a基础组件

天天用的积木:Text、按钮、输入、图、开关、进度、分隔。每个组件固定:概念 → XML → ★/○ → 易错 → ▶案例(含示意)。

📌 组件 = 可复用的 @Composable。布局已在D1,修饰在D2。汇总见组件总表

Text ★

概念

显示一段文字。样式优先走 MaterialTheme.typography,少写死字号,全 App 才统一。

和 XML 对照

TextView。XML 常靠 textAppearance / style;Compose 用 style / color / maxLines 等参数。

怎么用(★)

@Composable
fun TextBasics() {
    Text(
        text = "标题文字",
        style = MaterialTheme.typography.titleLarge,
        color = MaterialTheme.colorScheme.onSurface,
        maxLines = 1,
        overflow = TextOverflow.Ellipsis
    )
}

○ 偶尔:AnnotatedString 局部加粗/链接;自定义 fontFamily

易错点

  • 到处 fontSize = 18.sp → 主题一改全盘不一致。
  • 长文不设 maxLines + overflow → 把布局撑破。
▶ 标题 + 辅助说明两行样式
@Composable
fun TitleWithCaption() {
    Column {
        Text(
            "账号安全",
            style = MaterialTheme.typography.titleMedium
        )
        Text(
            "管理登录方式与验证设备",
            style = MaterialTheme.typography.bodySmall,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

💡 贴进 Studio 加 @Preview 看字阶。@Preview

Button / TextButton / OutlinedButton ★

概念

三种常见强调级别:实心主操作、描边次要、纯文字弱操作。都是 onClick + 内容 lambda(通常再放 Text / Icon)。

和 XML 对照

Button / Material MaterialButton 的 filled / outlined / text 样式。Compose 用不同函数名表达风格。

怎么用(★)

@Composable
fun ButtonBasics(onPrimary: () -> Unit) {
    Button(onClick = onPrimary, enabled = true) {
        Text("保存")
    }
    OutlinedButton(onClick = { }) {
        Text("取消")
    }
    TextButton(onClick = { }) {
        Text("了解更多")
    }
}

○:IconButtonFloatingActionButton(结构章 / 总表再展开)。

易错点

  • 把文案写在 Button 参数里而不是内容 lambda —— API 是 Button { Text(...) }
  • 主次不分:一屏多个实心 Button,视觉抢戏。
▶ 主按钮 + 次要文字按钮
@Composable
fun LoginActions(
    onLogin: () -> Unit,
    onForgot: () -> Unit
) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Button(onClick = onLogin) {
            Text("登录")
        }
        Spacer(modifier = Modifier.width(12.dp))
        TextButton(onClick = onForgot) {
            Text("忘记密码?")
        }
    }
}

OutlinedTextField / TextField ★

概念

受控输入:value + onValueChange 必须接状态。日常更常用描边款 OutlinedTextField

和 XML 对照

EditText / TextInputLayout。XML 用监听取文案;Compose 状态即文案。

怎么用(★)

@Composable
fun FieldBasics() {
    var name by remember { mutableStateOf("") }
    OutlinedTextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("用户名") },
        placeholder = { Text("请输入") },
        singleLine = true,
        isError = false,
        supportingText = null,
        modifier = Modifier.fillMaxWidth()
    )
}

○:visualTransformation(密码遮罩)、自定义 keyboardOptions。

易错点

  • 只写 value 不写 onValueChange → 打不了字。
  • 状态没 remember → 重组被清空(见状态章)。
▶ 账号输入:受控 value + 错误提示
@Composable
fun AccountField() {
    var account by remember { mutableStateOf("") }
    val error = account.isNotEmpty() && account.length < 3
    OutlinedTextField(
        value = account,
        onValueChange = { account = it },
        label = { Text("账号") },
        isError = error,
        supportingText = {
            if (error) Text("至少 3 个字符")
        },
        singleLine = true,
        modifier = Modifier.fillMaxWidth()
    )
}

要点:错误与否也是状态的函数,和登录按钮 enabled 同一套路。

Image ★ · AsyncImage(Coil)★

概念

本地资源用 Image(painterResource);网络图用 Coil 的 AsyncImage(异步 + 缓存)。装饰图 contentDescription = null;有含义的图要写描述(无障碍)。

和 XML 对照

ImageView + Glide/Coil。Compose 本地走 painter;网络仍推荐 Coil。

怎么用(★)

@Composable
fun LocalAvatar() {
    Image(
        painter = painterResource(R.drawable.ic_avatar),
        contentDescription = "头像",
        modifier = Modifier
            .size(48.dp)
            .clip(CircleShape)
    )
}

@Composable
fun NetworkAvatar(url: String) {
    AsyncImage(
        model = url,
        contentDescription = "头像",
        modifier = Modifier
            .size(48.dp)
            .clip(CircleShape)
    )
}

○:ContentScaleplaceholder / error 画家(Coil 参数)。

易错点

  • 网络图用同步解码堵主线程 —— 用 AsyncImage / rememberAsyncImagePainter。
  • 有意义的图标 contentDescription = null → 读屏读不到。
▶ 头像本地图 vs 网络图

依赖:Coil 的 coil-compose。本地放 res/drawable;网络传 URL 字符串即可。

Switch / Checkbox / RadioButton ★

概念

布尔或互斥选择:开关、勾选、单选。一律 checked + onCheckedChange(或 Radio 的 selected 模式)。

和 XML 对照

Switch / CheckBox / RadioButton。XML 监听改 UI;Compose 改状态即可。

怎么用(★)

@Composable
fun ToggleBasics() {
    var on by remember { mutableStateOf(true) }
    Switch(checked = on, onCheckedChange = { on = it })

    var checked by remember { mutableStateOf(false) }
    Checkbox(checked = checked, onCheckedChange = { checked = it })
}

易错点

  • 只改 checked 字面量、没有状态 → 点了不变。
  • 整行要可点时,把状态提升到 Row 的 clickable(见案例)。
▶ 设置里的开关行
@Composable
fun NotificationSwitchRow() {
    var enabled by remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { enabled = !enabled }
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("消息通知", modifier = Modifier.weight(1f))
        Switch(
            checked = enabled,
            onCheckedChange = { enabled = it }
        )
    }
}

CircularProgressIndicator / LinearProgressIndicator ★

概念

加载反馈:转圈或横条。不确定进度用默认动画;确定进度传 progress

和 XML 对照

ProgressBar(circular / horizontal)。

怎么用(★)

@Composable
fun ProgressBasics(loading: Boolean) {
    if (loading) {
        CircularProgressIndicator()
    }
    LinearProgressIndicator(
        progress = { 0.4f },
        modifier = Modifier.fillMaxWidth()
    )
}

易错点

  • 加载结束不把状态关掉 → 转圈永久转。
  • 阻塞主线程却指望 Indicator「表示在忙」——指示器只是 UI,耗时仍要丢后台。
▶ 提交中按钮旁小转圈
@Composable
fun SubmitRow(
    submitting: Boolean,
    onSubmit: () -> Unit
) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Button(
            onClick = onSubmit,
            enabled = !submitting
        ) {
            Text(if (submitting) "提交中…" else "提交")
        }
        if (submitting) {
            Spacer(modifier = Modifier.width(12.dp))
            CircularProgressIndicator(
                modifier = Modifier.size(24.dp),
                strokeWidth = 2.dp
            )
        }
    }
}

Spacer / Divider ★

概念

Spacer 占位空白;HorizontalDivider(或旧名 Divider)画分隔线。比硬写空 Text(" ") 干净。

和 XML 对照

Spacer ≈ 固定宽高的空 View / margin;Divider ≈ View 1dp + background。

怎么用(★)

@Composable
fun SpaceBasics() {
    Column {
        Text("上一段")
        Spacer(modifier = Modifier.height(16.dp))
        HorizontalDivider()
        Spacer(modifier = Modifier.height(16.dp))
        Text("下一段")
    }
}

易错点

  • 能用 Arrangement.spacedBy 就少堆 Spacer。
  • 分割线忘了水平 fillMaxWidth()(视 Material 版本 API 而定)。
▶ 表单分段
@Composable
fun FormSections() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("基本信息", style = MaterialTheme.typography.titleSmall)
        Spacer(modifier = Modifier.height(8.dp))
        OutlinedTextField(value = "", onValueChange = { }, label = { Text("昵称") })
        Spacer(modifier = Modifier.height(16.dp))
        HorizontalDivider()
        Spacer(modifier = Modifier.height(16.dp))
        Text("安全设置", style = MaterialTheme.typography.titleSmall)
    }
}

○ Slider · Chip · DropdownMenu

概念

用得到再查:滑动条、芯片标签、下拉菜单。主线先混个眼熟,细节回组件总表 / 官网。

和 XML 对照

Slider ≈ SeekBar;Chip ≈ Material Chip;Dropdown ≈ PopupMenu / Spinner。

怎么用(点到)

@Composable
fun OccasionalBasics() {
    var v by remember { mutableStateOf(0.3f) }
    Slider(value = v, onValueChange = { v = it })

    AssistChip(
        onClick = { },
        label = { Text("标签") }
    )
}

易错点

Dropdown 要自己管 expanded 状态;别和「偶尔才用」的复杂度纠缠进主线。

▶ 点到即可

需要完整菜单、筛选芯片组时,打开总表或官网组件页按需抄。

下一章:结构与容器(Scaffold / Card / Dialog / 顶栏底栏 / Preview)。
Android Compose 现代开发知识体系 · 主线必读 / 扩展选读。
术语表常驻;组件与属性先看分讲,再查两张总表。案例默认收起。