Jetpack Compose
声明式 UI 与 Navigation。
Jetpack Compose
这一课你将能做到
- 用 @Composable 函数构建声明式 UI,理解重组(Recomposition)的触发机制
- 使用 remember、mutableStateOf、StateFlow 管理 UI 状态,区分 Stateless 与 Stateful 组件
- 用 LazyColumn/LazyRow 构建高性能列表,实现 Navigation Compose 页面导航
为什么要学
Jetpack Compose 是 Android UI 的未来,Google 已停止对传统 View 体系的大规模投入。Compose 用 Kotlin 代码替代 XML,用状态驱动替代命令式调用,与 React 的函数组件 + Hooks 思维几乎同构。对于 Web 前端背景的开发者,这是最低成本的迁移路径。
人话:Compose 就是「Android 版的 React」。@Composable 函数 ≈ React 组件,remember ≈ useState,LaunchedEffect ≈ useEffect,LazyColumn ≈ react-window。你不需要学新的心智模型,只需要学新的 API 名字。
面试中,「Compose 重组原理」、「State 管理」、「与 View 的互操作」是高频考点。
先建立图像
传统 View 体系(命令式):
┌─────────────┐ findViewById ┌─────────────┐
│ Activity │ ────────────────────→ │ TextView │
│ │ setText("Hello") │ (实例) │
└─────────────┘ └─────────────┘
↑ 手动同步状态,容易遗漏
Compose 体系(声明式):
┌─────────────┐ 状态变化自动重组 ┌─────────────┐
│ ViewModel │ ────────────────────→ │ Compose 树 │
│ (StateFlow)│ 框架自动 diff │ (描述) │
└─────────────┘ └─────────────┘
↑ 状态唯一来源,UI 是状态的函数
跟着做
步骤 1:第一个 Compose 页面
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CodeRoadMapTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TrackListScreen()
}
}
}
}
}
@Composable
fun TrackListScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "学习路线",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
TrackCard(
title = "Android 基础",
lessonCount = 12,
isCompleted = true
)
Spacer(modifier = Modifier.height(8.dp))
TrackCard(
title = "Kotlin 入门",
lessonCount = 10,
isCompleted = false
)
}
}
@Composable
fun TrackCard(
title: String,
lessonCount: Int,
isCompleted: Boolean,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isCompleted) Icons.Filled.CheckCircle else Icons.Outlined.Circle,
contentDescription = null,
tint = if (isCompleted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "$lessonCount 节课",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
步骤 2:状态管理与交互
@Composable
fun InteractiveCounter() {
// remember 在重组时保留状态
var count by remember { mutableStateOf(0) }
var isExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "学习进度",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
// 线性进度条
LinearProgressIndicator(
progress = { count / 100f },
modifier = Modifier.fillMaxWidth()
)
Text("已完成 $count%")
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(onClick = { if (count < 100) count += 10 }) {
Text("学习 +10%")
}
Spacer(modifier = Modifier.width(8.dp))
OutlinedButton(onClick = { count = 0 }) {
Text("重置")
}
}
Spacer(modifier = Modifier.height(16.dp))
// 可展开卡片
Card(
onClick = { isExpanded = !isExpanded },
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = if (isExpanded) "收起详情 ▲" else "展开详情 ▼",
style = MaterialTheme.typography.titleMedium
)
if (isExpanded) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "这是详细的学习内容说明。状态驱动 UI 的好处是:数据和界面永远同步,不会出现「数据变了但忘记更新某个 TextView」的问题。",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
步骤 3:LazyColumn 高性能列表
@Composable
fun TrackLazyList() {
val tracks = remember {
listOf(
Track("and-intro", "Android 开发概览", 12, 85),
Track("and-components", "四大组件", 10, 60),
Track("and-ui", "传统 View UI", 8, 30),
Track("and-compose", "Jetpack Compose", 15, 10),
Track("and-arch", "架构模式", 9, 0),
Track("and-di", "依赖注入", 7, 0),
Track("and-storage", "本地存储", 11, 0),
Track("and-network", "网络层", 8, 0)
)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tracks,
key = { it.slug } // 唯一 key 优化重组
) { track ->
TrackItemCard(track = track)
}
}
}
@Composable
fun TrackItemCard(track: Track) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = track.title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${track.progress}%",
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = { track.progress / 100f },
modifier = Modifier.fillMaxWidth()
)
}
}
}
data class Track(
val slug: String,
val title: String,
val lessonCount: Int,
val progress: Int
)
步骤 4:Navigation Compose 页面导航
// 定义路由
sealed class Screen(val route: String) {
object TrackList : Screen("track_list")
object TrackDetail : Screen("track_detail/{trackSlug}") {
fun createRoute(trackSlug: String) = "track_detail/$trackSlug"
}
object Settings : Screen("settings")
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.TrackList.route
) {
composable(Screen.TrackList.route) {
TrackListScreen(
onTrackClick = { trackSlug ->
navController.navigate(Screen.TrackDetail.createRoute(trackSlug))
}
)
}
composable(
route = Screen.TrackDetail.route,
arguments = listOf(navArgument("trackSlug") { type = NavType.StringType })
) { backStackEntry ->
val trackSlug = backStackEntry.arguments?.getString("trackSlug") ?: ""
TrackDetailScreen(
trackSlug = trackSlug,
onBack = { navController.popBackStack() }
)
}
composable(Screen.Settings.route) {
SettingsScreen()
}
}
}
@Composable
fun TrackDetailScreen(trackSlug: String, onBack: () -> Unit) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("课程详情") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
Text(
text = "课程: $trackSlug",
style = MaterialTheme.typography.headlineMedium
)
}
}
}
步骤 5:ViewModel 集成
class TrackViewModel : ViewModel() {
private val _uiState = MutableStateFlow(TrackListUiState())
val uiState: StateFlow<TrackListUiState> = _uiState.asStateFlow()
init {
loadTracks()
}
private fun loadTracks() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
// 模拟网络请求
delay(1000)
_uiState.value = TrackListUiState(
tracks = listOf(
Track("and-intro", "Android 开发概览", 12, 85),
Track("and-components", "四大组件", 10, 60)
),
isLoading = false
)
}
}
fun refresh() {
loadTracks()
}
}
data class TrackListUiState(
val tracks: List<Track> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
@Composable
fun TrackListWithViewModel(
viewModel: TrackViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
when {
uiState.isLoading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
uiState.error != null -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("错误: ${uiState.error}")
}
}
else -> {
LazyColumn {
items(uiState.tracks) { track ->
TrackItemCard(track = track)
}
}
}
}
}
讲清楚要点
-
重组(Recomposition)是核心机制:当 State 变化时,Compose 框架会重新执行受影响的 @Composable 函数,生成新的 UI 树,然后 diff 更新。不是手动 setText,而是自动响应状态。
-
remember 在重组间保留状态:没有 remember,每次重组都会重新创建状态变量,导致用户输入丢失。rememberSaveable 在配置变更(如旋转)时也能保留。
-
Stateless 组件更易测试:把状态提升到父组件或 ViewModel,子组件只负责渲染。这叫做「状态提升」,与 React 的 Lifting State Up 完全一致。
-
LazyColumn 不是 RecyclerView 的简单替代:它天然支持动画、更灵活的 Item 类型、与 Compose 状态系统深度集成。key 参数帮助框架识别 Item 身份,优化重组。
-
Modifier 是 Compose 的「装饰器」:Modifier.padding(16.dp).fillMaxWidth().clickable { } 链式调用,每个 Modifier 包一层。顺序很重要,先 padding 再 fillMaxWidth 与反过来效果不同。
你可能会懵的地方
-
为什么我的 Compose 页面不更新?
检查状态是否用 mutableStateOf 创建,是否通过 by remember 委托。直接 var count = 0 不会触发重组,因为 Compose 不知道它是状态。
-
LaunchedEffect 和 SideEffect 有什么区别?
LaunchedEffect 在组合时启动协程,key 变化时重启。SideEffect 每次成功重组后执行,用于把 Compose 状态同步到非 Compose 代码(如分析 SDK)。
-
Compose 性能真的比 View 好吗?
理论上更好(避免 View 对象创建、自动 diff),但取决于写法。不当使用 remember、在组合中做耗时操作、Item 没有稳定 key,都会导致性能问题。
-
能在 Compose 里用传统 View 吗?
可以,用 AndroidView 包装。反之用 ComposeView 在 XML 布局里嵌入 Compose。互操作是渐进迁移的关键。
-
Preview 不显示怎么办?
检查函数是否无参或所有参数有默认值。@Preview 只能预览无参 Composable,或用 @PreviewParameter 提供测试数据。
错法对照
| 错法 | 问题 | 正解 |
|---|
var count = 0 直接赋值 | 不触发重组 | var count by remember { mutableStateOf(0) } |
| 在 Composable 里直接网络请求 | 阻塞组合,掉帧 | LaunchedEffect + ViewModel |
items(tracks) 不传 key | 重组时 Item 错位/闪烁 | items(items, key = { it.id }) |
在 remember 里初始化耗时对象 | 每次重组都执行 | remember { derivedStateOf { ... } } 或移到 ViewModel |
过度使用 Column 嵌套 | 深度嵌套导致测量多次 | 用 ConstraintLayout 或自定义 Layout |
卡点急救
-
重组太频繁,性能差:用 debugger 或 Layout Inspector 查看重组范围。把状态下沉到更小的组件,或用 derivedStateOf 减少不必要的状态读取。
-
TextField 输入时焦点丢失:检查是否每次重组都创建了新的 MutableState。确保用 remember 保留,且 onValueChange 正确更新状态。
-
Navigation 传参类型不匹配:NavType 必须与实际类型一致。复杂对象用 JSON 字符串或 Parcelable,或改用共享 ViewModel。
面试常问
-
Compose 重组的原理?
Compose 编译器插件在编译期把 @Composable 函数转成带 Composer 参数的代码。运行时,Composer 维护一个「插槽表」记录状态和位置。状态变化时,Composer 重新执行受影响的函数,生成新的 UI 树,与旧树 diff 后更新实际 View。
-
remember 和 rememberSaveable 的区别?
remember 只在重组间保留,配置变更(旋转、语言切换)后丢失。rememberSaveable 在配置变更时自动保存到 Bundle,重建后恢复。用于表单输入、滚动位置等需要持久化的状态。
-
Compose 如何与现有 View 体系共存?
ComposeView:在 XML/Fragment 中嵌入 Compose;2. AndroidView:在 Compose 中嵌入传统 View;3. ViewCompositionStrategy:控制 Compose 生命周期与 View 销毁时机。渐进迁移策略:新页面用 Compose,老页面逐步替换。
自测三问
- 解释
remember 在重组中的作用,以及什么情况下需要 rememberSaveable。
- LazyColumn 的
key 参数为什么能提升性能?
- 状态提升(State Hoisting)的好处是什么?举一个实际场景。
小结一句话
Jetpack Compose 是 Android 的声明式 UI 革命:状态驱动、自动重组、Kotlin 原生;与 React 思维同构,是 Web 前端迁移到 Android 的最短路径。
延伸