Jetpack Compose 是 Android 官方的现代 UI 工具包。它用 Kotlin 函数描述界面:数据变化时,Compose 会重新执行受影响的 UI 代码,把界面更新到新的状态。
本文按照"原理 → 特点 → 代码 → 使用场景"的节奏,带你完成一个最小任务清单,并串起 Compose 开发最常用的知识点。示例默认使用 Kotlin、Material 3、ViewModel 与 Navigation Compose。
版本提示:Compose 更新较快。新项目优先使用 Android Studio 的 Compose 模板,让 IDE 生成彼此兼容的 Kotlin、Compose Compiler、Compose BOM 与 Android Gradle Plugin 版本。本文刻意不写死版本号。
总览
| 知识点 | 常用 API | 核心作用 | 典型场景 |
|---|---|---|---|
| 可组合函数 | @Composable |
用函数描述 UI | 页面、组件、列表项 |
| 修饰符 | Modifier |
控制尺寸、间距、点击、绘制 | 绝大多数 UI 元素 |
| 基础布局 | Row、Column、Box |
横向、纵向、层叠排列 | 常规页面布局 |
| 状态 | remember、rememberSaveable |
保存 UI 状态并触发重组 | 输入框、开关、选中项 |
| 列表 | LazyColumn、LazyRow |
按需组合可见项 | 消息流、商品列表 |
| Material 3 | MaterialTheme、Scaffold |
统一颜色、字体和页面骨架 | App 主题、TopBar、FAB |
| 状态提升 | value + callback | 保持组件无状态、便于复用 | 表单、业务组件 |
| ViewModel | StateFlow |
承载业务状态并跨配置变化 | 页面状态、异步数据 |
| 副作用 | LaunchedEffect、DisposableEffect |
安全执行非 UI 工作 | 请求、监听、清理资源 |
| 导航 | NavHost、NavController |
管理页面与返回栈 | 多页面 App |
| View 互操作 | AndroidView、ComposeView |
渐进迁移旧页面 | 地图、WebView、遗留 XML |
1. Compose 是什么
原理
传统 View 通常按步骤修改控件:找到 TextView,再调用 setText()。Compose 更关心"当前状态下界面应该长什么样"。
kotlin
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
name 变化后,Compose 会重新执行依赖它的代码。这个过程叫 重组(Recomposition)。重组不是重建整个 Activity,也不意味着整个界面都会重绘;Compose 会尽量跳过输入未变化的部分。
特点
- UI 与状态关系直接,减少手动同步控件的代码。
- 组件就是 Kotlin 函数,容易拆分、预览和测试。
- 与 Kotlin 协程、Flow、ViewModel、Lifecycle 配合自然。
- 可以和原有 View/XML 混合使用,不必一次性重写项目。
适合的使用场景
- 新建 Android 页面或新项目。
- 需要快速迭代的业务 UI。
- 多状态页面、动态主题、动画和响应式布局。
- 在旧项目中逐页迁移 XML。
2. 创建第一个 Compose 工程
推荐方式
在 Android Studio 中选择支持 Compose 的空 Activity 模板。模板会自动完成以下配置:
- 启用 Compose 构建功能;
- 配置 Compose Compiler;
- 引入 Activity Compose 与 Material 3;
- 创建继承
ComponentActivity的入口 Activity。
手动配置时,模块级 build.gradle.kts 的核心结构如下。版本请交给 Version Catalog 或 Compose BOM 统一管理。
kotlin
android {
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
debugImplementation(libs.androidx.compose.ui.tooling)
}
Activity 入口
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QuickStartTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Greeting(name = "Compose")
}
}
}
}
}
setContent {} 是 View 世界进入 Compose 世界的入口。花括号里的内容就是这棵 Compose UI 树的根节点。
3. 可组合函数 Composable
原理
被 @Composable 标记的函数可以调用其他可组合函数。它通常接收数据和事件回调,不直接持有业务对象。
kotlin
@Composable
fun UserCard(
name: String,
online: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
onClick = onClick,
modifier = modifier,
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = name, style = MaterialTheme.typography.titleMedium)
Text(text = if (online) "在线" else "离线")
}
}
}
特点
- 函数名通常使用大驼峰,例如
UserCard。 modifier建议作为可选参数暴露给调用者。- 参数应尽量描述 UI 所需的最小数据。
- 事件通过 lambda 向上通知,如
onClick、onValueChange。
Preview 预览
kotlin
@Preview(showBackground = true)
@Composable
private fun UserCardPreview() {
QuickStartTheme {
UserCard(
name = "Ada",
online = true,
onClick = {},
)
}
}
Preview 适合检查独立组件。预览里尽量传入静态假数据,不要直接启动网络请求或依赖真实数据库。
4. Modifier:Compose 布局的通用语言
原理
Modifier 是不可变的修饰符链,用来改变元素的布局、绘制、交互与语义。链式调用的顺序会影响结果。
kotlin
Text(
text = "开始学习",
modifier = Modifier
.padding(16.dp)
.background(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(12.dp),
)
.clickable { /* 点击事件 */ }
.padding(horizontal = 20.dp, vertical = 12.dp),
)
这里的第一次 padding 是组件外间距,第二次 padding 是背景内部的留白。
常用 Modifier
| API | 作用 |
|---|---|
fillMaxWidth() |
占满可用宽度 |
size() / width() / height() |
设置尺寸 |
padding() |
添加布局留白 |
background() |
绘制背景 |
clip() |
裁剪形状 |
clickable() |
增加点击行为与语义 |
weight() |
按比例分配 Row/Column 剩余空间 |
align() |
在父布局作用域中对齐 |
semantics {} |
补充无障碍与测试语义 |
使用建议
- 组件内部使用
modifier参数时,通常只应用一次,并放在最外层节点。 - 不要把调用方传入的
modifier擅自替换为Modifier。 - 视觉间距、点击范围异常时,先检查修饰符顺序。
5. Row、Column 与 Box
原理
Row:子项从左到右排列;Column:子项从上到下排列;Box:子项可以层叠;Spacer:创建明确的空白;Arrangement与Alignment:控制主轴分布和交叉轴对齐。
kotlin
@Composable
fun ProfileHeader(
name: String,
subtitle: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Text(text = name.take(1))
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = name, style = MaterialTheme.typography.titleMedium)
Text(text = subtitle, style = MaterialTheme.typography.bodyMedium)
}
IconButton(onClick = { }) {
Icon(Icons.Default.MoreVert, contentDescription = "更多")
}
}
}
使用场景
- Row:工具栏、头像信息、横向按钮组。
- Column:表单、文章、设置页。
- Box:角标、遮罩、图片上的文字、加载层。
避坑:不要用嵌套很多层的 Row/Column 模拟复杂滚动列表。大量数据请使用 Lazy 布局。
6. 状态与重组
原理
Compose 观察 State<T>。当它的 value 变化时,读取过该值的可组合函数会进入重组。
kotlin
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "点击次数:$count")
Spacer(modifier = Modifier.width(12.dp))
Button(onClick = { count++ }) {
Text("+1")
}
}
}
remember 能跨重组保存值,但 Activity 重建后可能丢失。简单且可保存的 UI 状态可以用 rememberSaveable。
kotlin
@Composable
fun NameInput() {
var name by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("昵称") },
singleLine = true,
)
}
三种常见状态位置
| 状态类型 | 推荐位置 | 示例 |
|---|---|---|
| 仅影响当前小组件 | remember |
展开/收起、临时动画状态 |
| 需要跨 Activity 重建 | rememberSaveable |
输入文本、Tab 下标 |
| 业务状态或跨组件共享 | ViewModel |
页面数据、加载结果、用户操作 |
常见错误
kotlin
// 错误:每次重组都会重新赋值,且普通变量变化不会通知 Compose。
var count = 0
UI 需要响应数据变化时,应把可观察状态放在稳定的持有者中。
7. 状态提升:让组件更容易复用
原理
状态提升是把组件内部的状态拆成两个参数:
value:当前状态;onValueChange:请求修改状态的事件。
kotlin
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier.fillMaxWidth(),
placeholder = { Text("搜索任务") },
leadingIcon = {
Icon(Icons.Default.Search, contentDescription = null)
},
singleLine = true,
)
}
调用方负责真正保存数据:
kotlin
@Composable
fun SearchScreen() {
var query by rememberSaveable { mutableStateOf("") }
SearchBar(
query = query,
onQueryChange = { query = it },
)
}
特点
- 子组件变成无状态组件,预览和测试更简单。
- 父组件拥有唯一数据源,避免多个副本不同步。
- 组件只暴露事件,不需要知道 ViewModel 或导航对象。
8. LazyColumn:高效显示列表
原理
Column.verticalScroll() 会组合所有子项;LazyColumn 只组合当前可见区域附近的项目,更适合大量或动态数据。
kotlin
data class Task(
val id: Long,
val title: String,
val completed: Boolean,
)
@Composable
fun TaskList(
tasks: List<Task>,
onToggle: (Long) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
items(
items = tasks,
key = { task -> task.id },
) { task ->
TaskItem(
task = task,
onToggle = { onToggle(task.id) },
modifier = Modifier.animateItem(),
)
}
}
}
@Composable
private fun TaskItem(
task: Task,
onToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = task.completed,
onCheckedChange = { onToggle() },
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = task.title,
modifier = Modifier.weight(1f),
textDecoration = if (task.completed) {
TextDecoration.LineThrough
} else {
TextDecoration.None
},
)
}
}
}
特点
items()用于列表数据,item {}用于单个固定内容。- 为可变列表提供稳定且唯一的
key,有利于保留项目状态和动画。 - 不要用数组下标作为会插入、删除或排序的数据的长期身份。
LazyRow用法类似,方向改为横向。
9. Material 3 与 Scaffold
原理
MaterialTheme 提供颜色、字体和形状;Scaffold 提供 TopBar、BottomBar、FAB、Snackbar 与内容区域的页面骨架。
kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskScreen(
tasks: List<Task>,
onAdd: () -> Unit,
onToggle: (Long) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("任务清单") })
},
floatingActionButton = {
FloatingActionButton(onClick = onAdd) {
Icon(Icons.Default.Add, contentDescription = "添加任务")
}
},
) { innerPadding ->
TaskList(
tasks = tasks,
onToggle = onToggle,
modifier = Modifier.padding(innerPadding),
)
}
}
主题
kotlin
private val LightColors = lightColorScheme(
primary = Color(0xFF65558F),
secondary = Color(0xFF006A60),
)
@Composable
fun QuickStartTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = LightColors,
typography = Typography(),
content = content,
)
}
使用建议
- 业务组件优先读取
MaterialTheme.colorScheme,避免散落硬编码颜色。 Scaffold的innerPadding必须传给内容,否则内容可能被 TopBar 遮挡。- 图标按钮需要有意义的
contentDescription;纯装饰图标可传null。
10. ViewModel + StateFlow:承载业务状态
原理
ViewModel 负责业务状态与事件处理,Composable 负责展示状态与上报用户操作。数据通常单向流动:
text
StateFlow<UiState> ──> UI
ViewModel <── UI Event
ViewModel
kotlin
data class TaskUiState(
val tasks: List<Task> = emptyList(),
val isLoading: Boolean = false,
val message: String? = null,
)
class TaskViewModel : ViewModel() {
private val _uiState = MutableStateFlow(
TaskUiState(
tasks = listOf(
Task(1, "认识 Composable", true),
Task(2, "掌握状态提升", false),
Task(3, "完成任务列表", false),
)
)
)
val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
fun toggleTask(id: Long) {
_uiState.update { state ->
state.copy(
tasks = state.tasks.map { task ->
if (task.id == id) {
task.copy(completed = !task.completed)
} else {
task
}
}
)
}
}
}
在 Compose 中收集状态
kotlin
@Composable
fun TaskRoute(
viewModel: TaskViewModel = viewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
TaskScreen(
tasks = uiState.tasks,
onAdd = { /* 打开新增页面 */ },
onToggle = viewModel::toggleTask,
)
}
特点
collectAsStateWithLifecycle()会结合生命周期收集 Flow。- 对外暴露只读
StateFlow,内部保留可变实现。 UiState使用不可变数据,更新时创建新值。- 不要把
Context、Activity或NavController长期保存到 ViewModel。
11. 副作用 Side Effects
原理
Composable 可能多次执行,因此网络请求、启动协程、注册监听等操作不能直接写在函数体里。Compose 提供副作用 API,让这些工作和 UI 生命周期对齐。
LaunchedEffect
当 key 进入组合或发生变化时启动协程;离开组合时协程自动取消。
kotlin
@Composable
fun UserScreen(
userId: String,
loadUser: suspend (String) -> Unit,
) {
LaunchedEffect(userId) {
loadUser(userId)
}
}
DisposableEffect
适合需要成对注册和清理的资源。
kotlin
@Composable
fun LifecycleLogger(owner: LifecycleOwner = LocalLifecycleOwner.current) {
DisposableEffect(owner) {
val observer = LifecycleEventObserver { _, event ->
Log.d("Lifecycle", event.name)
}
owner.lifecycle.addObserver(observer)
onDispose {
owner.lifecycle.removeObserver(observer)
}
}
}
rememberCoroutineScope
适合由点击事件触发、且生命周期绑定当前组合的协程。
kotlin
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("保存成功")
}
}
) {
Text("保存")
}
选择建议
| 需求 | API |
|---|---|
| 随 key 变化执行挂起任务 | LaunchedEffect(key) |
| 注册并清理监听器 | DisposableEffect(key) |
| 点击后启动协程 | rememberCoroutineScope() |
| 把 Compose 状态暴露给非 Compose 对象 | SideEffect |
| 把多个状态计算成派生状态 | derivedStateOf |
12. Navigation Compose 页面导航
依赖
kotlin
dependencies {
implementation(libs.androidx.navigation.compose)
}
最小导航图
kotlin
@Composable
fun AppNavHost() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "tasks",
) {
composable("tasks") {
TaskListRoute(
onTaskClick = { taskId ->
navController.navigate("task/$taskId")
}
)
}
composable(
route = "task/{taskId}",
arguments = listOf(
navArgument("taskId") { type = NavType.LongType }
),
) { backStackEntry ->
val taskId = backStackEntry.arguments?.getLong("taskId") ?: return@composable
TaskDetailScreen(
taskId = taskId,
onBack = navController::navigateUp,
)
}
}
}
这里的 TaskListRoute 表示列表页入口;它需要把 onTaskClick 继续传给具体列表项。这样页面只发出"打开任务"的事件,不直接持有导航对象。
特点
- 页面接收
onBack、onTaskClick等回调,不必直接依赖NavController。 - 导航参数尽量只传 ID,再从数据层加载完整对象。
- 生产项目可采用 Navigation Compose 当前稳定版支持的类型安全路由,减少字符串拼写错误。
navigateUp()适合处理返回行为。
13. 与传统 View/XML 混合使用
在 Compose 中使用 View
kotlin
@Composable
fun LegacyWebView(url: String) {
AndroidView(
factory = { context ->
WebView(context).apply {
webViewClient = WebViewClient()
}
},
update = { webView ->
if (webView.url != url) {
webView.loadUrl(url)
}
},
modifier = Modifier.fillMaxSize(),
)
}
在 XML 中嵌入 Compose
xml
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
kotlin
findViewById<ComposeView>(R.id.compose_view).setContent {
QuickStartTheme {
UserCard(
name = "Compose",
online = true,
onClick = {},
)
}
}
使用场景
- 地图、播放器、WebView 等已有成熟 View 组件。
- 大型旧项目逐页迁移。
- 先用 Compose 开发新功能,再逐步替换历史 XML。
14. 性能与调试
核心原则
Compose 性能优化的重点不是"避免所有重组",而是让状态范围合理、参数稳定,并避免在重组时重复做昂贵工作。
kotlin
@Composable
fun FilteredTaskList(
tasks: List<Task>,
query: String,
) {
val filteredTasks by remember(tasks, query) {
derivedStateOf {
tasks.filter { it.title.contains(query, ignoreCase = true) }
}
}
TaskList(
tasks = filteredTasks,
onToggle = {},
)
}
常见建议
- 列表项使用稳定
key。 - 昂贵计算使用
remember,并提供正确的 key。 - 不要在 Composable 函数体里直接请求网络、读文件或创建大型对象。
- 先用 Layout Inspector、系统跟踪和基准测试定位问题,再优化。
- 对高频变化值,可使用
derivedStateOf降低不必要的 UI 更新。 - 动画或滚动卡顿时,检查是否在每一帧分配大量对象。
常见误区
| 误区 | 正确理解 |
|---|---|
| 重组等于整个页面重绘 | Compose 会定位读取了变化状态的部分,并尝试跳过其他节点 |
remember 可以永久保存数据 |
它主要跨重组保存;业务数据应放入 ViewModel/数据层 |
| 所有状态都放 ViewModel | 纯 UI 临时状态放在最接近使用处更清晰 |
Column 可以替代所有列表 |
大数据列表应使用 Lazy 布局 |
| Modifier 顺序无所谓 | 顺序会改变尺寸、点击范围和绘制结果 |
15. 一个可运行的最小任务清单
下面把状态、列表、Material 3 与状态提升组合到一个文件中。把它放入 Compose 模板项目即可运行。
kotlin
data class SimpleTask(
val id: Int,
val title: String,
val done: Boolean = false,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoApp() {
var input by rememberSaveable { mutableStateOf("") }
var nextId by rememberSaveable { mutableIntStateOf(3) }
var tasks by remember {
mutableStateOf(
listOf(
SimpleTask(1, "创建 Compose 项目", true),
SimpleTask(2, "完成第一个列表"),
)
)
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Compose Todo") })
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
label = { Text("新任务") },
singleLine = true,
)
Button(
enabled = input.isNotBlank(),
onClick = {
tasks = tasks + SimpleTask(nextId++, input.trim())
input = ""
},
) {
Text("添加")
}
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(tasks, key = { it.id }) { task ->
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = task.done,
onCheckedChange = { checked ->
tasks = tasks.map {
if (it.id == task.id) {
it.copy(done = checked)
} else {
it
}
}
},
)
Text(
text = task.title,
modifier = Modifier.weight(1f),
)
IconButton(
onClick = {
tasks = tasks.filterNot { it.id == task.id }
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除 ${task.title}",
)
}
}
}
}
}
}
}
}
这个示例把状态保存在页面内,适合学习。接入数据库或网络后,应把任务数据与增删改逻辑迁移到 ViewModel 和 Repository。
常见选择建议
| 需求 | 推荐方案 |
|---|---|
| 少量、固定、无需滚动的内容 | Column / Row |
| 长列表或不确定数量的数据 | LazyColumn / LazyRow |
| 仅当前组件使用的临时状态 | remember |
| 需要跨 Activity 重建的简单 UI 状态 | rememberSaveable |
| 页面级业务状态 | ViewModel + StateFlow |
| 状态由多个值计算得出 | derivedStateOf 或在 ViewModel 中计算 |
| 进入页面后加载数据 | ViewModel 初始化,或谨慎使用 LaunchedEffect |
| 注册系统监听并在离开时清理 | DisposableEffect |
| 新项目统一视觉系统 | Material 3 主题 |
| 旧项目渐进迁移 | ComposeView + AndroidView |
面试回答模板
如果被问"Jetpack Compose 的核心是什么",可以这样回答:
Jetpack Compose 是 Android 的声明式 UI 工具包。开发者用 Composable 函数描述状态对应的界面,状态变化后 Compose 通过重组更新读取该状态的 UI。项目中通常通过状态提升保持组件无状态,再用 ViewModel 和 StateFlow 管理页面级业务状态。副作用使用 LaunchedEffect、DisposableEffect 等 API 与组合生命周期对齐,列表使用 Lazy 布局并提供稳定 key。
如果继续追问重组,可补充三点:
- 重组可能频繁发生,因此 Composable 应尽量无副作用、执行快速。
- 重组不等于 Activity 重建,也不等于整棵 UI 树重绘。
- 状态读取的位置决定了受影响的重组范围,应把状态放在合理层级。
学习建议
建议按以下顺序练习:
- 用
Text、Button、Row、Column还原一个静态页面。 - 用
rememberSaveable完成计数器和输入框。 - 用状态提升拆出无状态组件。
- 用
LazyColumn完成可增删改的任务列表。 - 把页面状态迁移到
ViewModel + StateFlow。 - 增加详情页,练习 Navigation Compose。
- 加入加载、空数据、错误三种页面状态。
- 最后再学习动画、自定义 Layout、Canvas 与性能基准测试。
只记住一句话:UI 是状态的函数,事件负责请求改变状态。 抓住这条主线,Compose 的大部分 API 都会变得容易理解。