CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Android 开发入门/课时/Jetpack Compose
RawBlame
课时目录12 节

Android 开发入门

  1. 01Android 开发概览
  2. 02四大组件
  3. 03传统 View UI
  4. 04Jetpack Compose
  5. 05架构模式
  6. 06依赖注入
  7. 07本地存储
  8. 08网络层
  9. 09异步与响应式
  10. 10Firebase 生态
  11. 11测试与质量
  12. 12综合练习

Jetpack Compose

声明式 UI 与 Navigation。

40 分钟

Jetpack Compose

这一课你将能做到

  1. 用 @Composable 函数构建声明式 UI,理解重组(Recomposition)的触发机制
  2. 使用 remember、mutableStateOf、StateFlow 管理 UI 状态,区分 Stateless 与 Stateful 组件
  3. 用 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)
                }
            }
        }
    }
}

讲清楚要点

  1. 重组(Recomposition)是核心机制:当 State 变化时,Compose 框架会重新执行受影响的 @Composable 函数,生成新的 UI 树,然后 diff 更新。不是手动 setText,而是自动响应状态。

  2. remember 在重组间保留状态:没有 remember,每次重组都会重新创建状态变量,导致用户输入丢失。rememberSaveable 在配置变更(如旋转)时也能保留。

  3. Stateless 组件更易测试:把状态提升到父组件或 ViewModel,子组件只负责渲染。这叫做「状态提升」,与 React 的 Lifting State Up 完全一致。

  4. LazyColumn 不是 RecyclerView 的简单替代:它天然支持动画、更灵活的 Item 类型、与 Compose 状态系统深度集成。key 参数帮助框架识别 Item 身份,优化重组。

  5. 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

卡点急救

  1. 重组太频繁,性能差:用 debugger 或 Layout Inspector 查看重组范围。把状态下沉到更小的组件,或用 derivedStateOf 减少不必要的状态读取。

  2. TextField 输入时焦点丢失:检查是否每次重组都创建了新的 MutableState。确保用 remember 保留,且 onValueChange 正确更新状态。

  3. Navigation 传参类型不匹配:NavType 必须与实际类型一致。复杂对象用 JSON 字符串或 Parcelable,或改用共享 ViewModel。

面试常问

  • Compose 重组的原理? Compose 编译器插件在编译期把 @Composable 函数转成带 Composer 参数的代码。运行时,Composer 维护一个「插槽表」记录状态和位置。状态变化时,Composer 重新执行受影响的函数,生成新的 UI 树,与旧树 diff 后更新实际 View。

  • remember 和 rememberSaveable 的区别? remember 只在重组间保留,配置变更(旋转、语言切换)后丢失。rememberSaveable 在配置变更时自动保存到 Bundle,重建后恢复。用于表单输入、滚动位置等需要持久化的状态。

  • Compose 如何与现有 View 体系共存?

    1. ComposeView:在 XML/Fragment 中嵌入 Compose;2. AndroidView:在 Compose 中嵌入传统 View;3. ViewCompositionStrategy:控制 Compose 生命周期与 View 销毁时机。渐进迁移策略:新页面用 Compose,老页面逐步替换。

自测三问

  1. 解释 remember 在重组中的作用,以及什么情况下需要 rememberSaveable。
  2. LazyColumn 的 key 参数为什么能提升性能?
  3. 状态提升(State Hoisting)的好处是什么?举一个实际场景。

小结一句话

Jetpack Compose 是 Android 的声明式 UI 革命:状态驱动、自动重组、Kotlin 原生;与 React 思维同构,是 Web 前端迁移到 Android 的最短路径。

延伸

  • Compose 官方文档
  • Compose 状态管理指南
  • 下一课:架构模式——MVVM、Repository 与分层设计。
加载本节练习…
← 上一课传统 View UI下一课 →架构模式