Jetpack Compose 从零到实战:Android 声明式 UI 开发指南
1. 项目概述:为什么现在必须学Compose?
如果你是一个Android开发者,最近打开招聘软件,或者看看身边同事的项目,大概率会听到一个词:Jetpack Compose。它不再是“未来可期”的技术,而是正在成为Android原生UI开发的“现在进行时”。我最近刚用Compose重构了一个老项目中的几个复杂页面,从最初的“这语法真奇怪”到后来的“真香”,整个过程感触颇深。所以,我想从一个一线开发者的角度,和你聊聊如何真正地从零开始上手Compose,避开我踩过的那些坑,快速把它用到实际项目里。
简单说,Jetpack Compose是Google推出的用于构建Android原生UI的现代工具包。它彻底抛弃了我们用了十多年的XML布局和findViewById那一套,改用Kotlin代码来声明UI。这意味着,你的UI逻辑和业务逻辑终于可以用同一种语言(Kotlin)写在一起了,再也不用在.xml和.kt文件之间来回切换。对于新手而言,最大的好处是学习曲线可能比想象中平缓——只要你理解了几个核心思想。而对于有经验的开发者,它带来的开发效率提升和代码可维护性的改善是实实在在的。
这篇内容就是为你准备的,无论你是刚入门Android,还是想从传统View系统迁移过来的老手。我会假设你具备基础的Kotlin知识,然后带你把Compose的核心概念、常用组件、状态管理、主题定制,一直到像“下拉刷新和上拉加载”这种实战功能封装,全都过一遍。目标很明确:看完之后,你不仅能理解Compose是怎么工作的,更能有底气在自己的下一个需求或项目中尝试使用它。
2. 核心理念与思想转变:从命令式到声明式
上手Compose,第一个要跨越的坎不是语法,而是编程思想的转变。我们习惯了十几年的Android开发方式是命令式的。回想一下你是怎么更新一个TextView的文本的:你先通过findViewById或ViewBinding拿到这个TextView对象的引用,然后在某个事件(比如按钮点击、网络回调)里,调用textView.text = “新内容”来命令它改变。UI的状态(显示什么文本)是由你通过代码命令去改变的,并且你需要自己管理这个TextView对象在整个生命周期中的存在。
Compose采用的是声明式UI范式。你不需要先创建UI对象然后命令它改变。相反,你只需要描述在任何给定状态下,UI应该长什么样。当状态(State)发生变化时,Compose会自动找到UI中依赖了该状态的部分,并只刷新这一小部分。这个过程叫做重组。
2.1 一个简单的思想实验
假设我们要显示一个计数器,点击按钮数字加1。
命令式(传统View):
- 在XML里定义一个
TextView和一个Button。 - 在Activity里,找到这两个View。
- 定义一个变量
count = 0。 - 给按钮设置点击监听器,在监听器里:
count++,然后textView.text = count.toString()。
- 在XML里定义一个
声明式(Compose):
- 定义一个可观察的状态:
var count by mutableStateOf(0)。 - 编写一个
Composable函数(比如叫Counter)来描述UI。 - 在这个函数里,你写:
Text(text = “Count: $count”)和Button(onClick = { count++ }) { Text(“Add”) }。
- 定义一个可观察的状态:
看出区别了吗?在Compose版本里,Text组件并没有一个“对象”被你持有。你只是声明:“这里要显示一个文本,它的内容是count变量的值”。当按钮被点击,count状态改变,Compose框架会自动重新调用Counter这个函数(即重组),然后计算出新的UI描述(文本显示“Count: 1”),并高效地更新到屏幕上。你从未手动去修改任何一个UI组件。
注意:这个“自动”是Compose魅力的核心,但也容易引发误区。重组不是重建,Compose非常智能,它会尽量跳过不需要更新的部分,并且重用已有的UI节点。但如果你在
Composable函数里写了副作用(比如网络请求、随机数生成),它可能会被频繁调用。所以,牢记:Composable函数应该是一个“纯函数”,给定相同的输入(参数和状态),应该返回相同的UI描述。
2.2 可组合函数:一切的基石
在Compose中,所有UI都是由Composable函数构建的。用@Composable注解标记一个函数,它就可以在里面调用其他Composable函数来构建UI。
@Composable fun Greeting(name: String) { // 这是一个Composable函数 Text(text = "Hello, $name!") }几个关键特点:
- 命名约定:通常使用帕斯卡命名法(大驼峰),让它看起来像一个自定义的UI组件。
- 不返回任何值(返回
Unit):它“发出”UI,而不是返回一个对象。 - 可以快速预览:在Android Studio中,加上
@Preview注解,无需运行应用就能在IDE里看到UI效果,这对开发效率是巨大的提升。 - 可以接受参数:参数是
Composable函数的数据输入,当参数变化时,函数会重组。
思想的转变需要一点时间来适应,但一旦理解,你会发现自己写UI代码的思路变得异常清晰和直接:数据驱动UI。
3. 基础构建块:常用组件与布局
理解了思想,我们来看看Compose给我们提供了哪些“积木”。这些基础组件在androidx.compose.material(Material Design风格)或androidx.compose.foundation(更基础的组件)包中。
3.1 基础显示组件
Text: 显示文本。比传统TextView功能更强大且易用。Text( text = "Hello Compose", color = Color.Blue, fontSize = 18.sp, // 注意:单位是 .sp (缩放像素) fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp) // 修饰符,后面详细讲 )Image: 显示图片。可以从资源、网络(需配合库)或Painter加载。Image( painter = painterResource(id = R.drawable.my_icon), contentDescription = "应用图标", // 无障碍功能描述,必须提供! modifier = Modifier.size(48.dp) )
3.2 基础交互组件
Button: 按钮。它的onClick参数接收一个挂起函数,非常方便。var clicked by remember { mutableStateOf(false) } Button(onClick = { clicked = !clicked }) { Text(if (clicked) "已点击" else "点击我") }TextField/OutlinedTextField: 文本输入框。状态管理是重点。var text by remember { mutableStateOf("") } OutlinedTextField( value = text, onValueChange = { newText -> text = newText }, label = { Text("请输入用户名") }, singleLine = true )实操心得:
TextField的值必须由状态控制(value),并通过onValueChange回调来更新状态。这是典型的“单向数据流”思想,保证了UI永远是状态的忠实反映。不要试图在Composable外直接修改TextField的显示值。
3.3 布局组件
布局决定了子元素的排列方式。Compose的布局思想也是声明式的。
Column: 垂直排列子项。相当于垂直的LinearLayout。Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally // 子项水平居中 ) { Text("第一行") Text("第二行") Button(onClick = {}) { Text("按钮") } }Row: 水平排列子项。相当于水平的LinearLayout。Box: 堆叠子项。相当于FrameLayout。常用于叠加元素(如文字浮于图片之上)或精确控制位置。Box(modifier = Modifier.size(100.dp)) { Image(painter = ..., contentDescription = ...) Text( text = "角标", modifier = Modifier.align(Alignment.TopEnd) // 对齐到Box的右上角 ) }LazyColumn/LazyRow: 相当于RecyclerView。用于高效显示长列表。这是必须掌握的核心组件。val items = listOf("Item A", "Item B", "Item C", ...) LazyColumn { items(items) { item -> Text(text = item, modifier = Modifier.padding(16.dp)) } // 或者使用 itemsIndexed 如果你需要索引 itemsIndexed(items) { index, item -> Text(text = "$index: $item") } }注意事项:
LazyColumn的items函数在列表数据变化时,默认不会做差异化比较(Diff),直接整体刷新。为了性能,如果列表项是数据类,建议为其实现equals和hashCode方法,或者使用items函数的key参数手动指定唯一键,这样Compose就能智能地只重组发生变化的项。
3.4 强大的工具:修饰符(Modifier)
Modifier是Compose中一个革命性的设计。它以一种链式调用的方式,为组件添加装饰或行为,如尺寸、边距、背景、点击事件等。一个组件可以没有参数,但几乎不会没有Modifier。
Box( modifier = Modifier .fillMaxWidth() // 充满最大宽度 .height(200.dp) // 固定高度 .background(Color.LightGray) // 背景色 .padding(16.dp) // 内边距 .clip(RoundedCornerShape(8.dp)) // 剪裁圆角 .clickable { /* 处理点击 */ } // 点击事件 .then(SomeOtherModifier) // 可以组合其他自定义Modifier ) { // 内容 }链式顺序很重要!修饰符的应用顺序是从左到右(从外到内)的。例如,.padding(10.dp).background(Color.Red)和.background(Color.Red).padding(10.dp)效果完全不同。前者背景色会延伸到padding区域,后者背景色只在padding内部。
4. 状态管理:Compose的“心脏”
如果说Composable函数是身体,那么状态(State)就是让身体活动起来的灵魂。管理好状态,是写好Compose应用的关键。
4.1 状态与重组
我们之前提到,状态变化触发重组。Compose中,状态需要用特定的API来创建,以便框架能够跟踪其变化。
mutableStateOf(): 创建可观察的状态。通常与remember结合使用。remember: 将计算或对象存储在组合中,在重组期间保持其值。如果remember的key变化,它会重新计算。
@Composable fun Counter() { // count 状态会被 remember 在组合中 // 当 Counter 重组时,只要 key 没变,count 就会保持之前的值 var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } }没有remember的话,每次重组count都会被重置为0,按钮点击就无效了。
4.2 状态提升
这是一个至关重要的模式。当多个Composable函数需要读取或修改同一状态时,或者状态需要在重组后存活时,应该将状态“提升”到它们共同的、足够高的父级中。
// 状态提升到调用方 @Composable fun ParentScreen() { var text by remember { mutableStateOf("") } // 状态在这里定义 // 将状态和事件回调作为参数传递给子组件 ChildInputField(value = text, onValueChange = { newText -> text = newText }) DisplayText(text = text) } @Composable fun ChildInputField(value: String, onValueChange: (String) -> Unit) { OutlinedTextField(value = value, onValueChange = onValueChange, label = { Text("输入") }) } @Composable fun DisplayText(text: String) { Text("你输入了:$text") }这样做的好处是:
- 单一数据源:状态只有一个“真相来源”,避免不一致。
- 可测试性:子组件
ChildInputField和DisplayText不持有状态,只通过参数接收,可以单独测试。 - 可复用性:子组件与具体状态解耦,可以在不同场景下复用。
4.3 ViewModel与状态
在真实的App中,业务逻辑和持久化状态通常放在ViewModel中。Compose通过viewModel()函数或hiltViewModel()(如果使用Hilt)来获取ViewModel实例。ViewModel中的状态应该使用Compose运行时库中的状态持有者,如mutableStateOf,或者使用StateFlow/LiveData并将其转换为Compose状态。
推荐方式:使用StateFlow+collectAsStateWithLifecycle
// ViewModel class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow(MyUiState()) val uiState: StateFlow<MyUiState> = _uiState.asStateFlow() fun updateData() { // 更新 _uiState viewModelScope.launch { _uiState.update { it.copy(loading = true) } // ... 执行逻辑 _uiState.update { it.copy(loading = false, data = result) } } } } // Composable Screen @Composable fun MyScreen(viewModel: MyViewModel = viewModel()) { // 使用 collectAsStateWithLifecycle,它与生命周期感知,在后台时停止收集,节省资源 val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.loading -> LoadingScreen() uiState.error != null -> ErrorScreen(uiState.error) else -> DataScreen(uiState.data) } }重要提示:
collectAsStateWithLifecycle在androidx.lifecycle:lifecycle-runtime-compose库中,这是目前处理StateFlow/LiveData与Compose状态转换的最佳实践,能有效避免因应用处于后台时不必要的重组和资源浪费。
5. 主题与样式:打造一致的设计语言
Material Design 3 (MD3) 是当前Compose Material库的默认设计语言。我们可以轻松地自定义应用的主题。
5.1 定义颜色、字体和形状
在ui.theme包下,通常有Color.kt,Type.kt,Shape.kt和Theme.kt文件。
// Color.kt val Purple80 = Color(0xFFD0BCFF) val PurpleGrey80 = Color(0xFFCCC2DC) // ... 定义你的调色板 // Type.kt val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp ) // ... 定义其他文本样式 ) // Shape.kt val Shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), large = RoundedCornerShape(0.dp) )5.2 应用主题
在入口Composable(通常是MainActivity的setContent里)用MaterialTheme包裹你的内容。
@Composable fun MyApp(content: @Composable () -> Unit) { MaterialTheme( colorScheme = LightColorScheme, // 使用定义的颜色方案 typography = Typography, shapes = Shapes, content = content ) } // 在Activity中 class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApp { // 你的应用根组件,例如 NavHost AppNavigation() } } } }应用主题后,组件会自动使用主题中定义的属性。例如,Button会使用主题中的primary颜色。
5.3 动态主题(深色/浅色模式)
Compose对深色模式的支持是原生的。你可以在主题中提供两套颜色方案。
@Composable fun MyApp( darkTheme: Boolean = isSystemInDarkTheme(), // 检查系统是否深色模式 content: @Composable () -> Unit ) { val colorScheme = if (darkTheme) { DarkColorScheme // 你定义的深色配色 } else { LightColorScheme // 你定义的浅色配色 } MaterialTheme( colorScheme = colorScheme, typography = Typography, shapes = Shapes, content = content ) }在组件内部,你可以通过MaterialTheme.colorScheme.primary来获取当前主题下的颜色,它会根据深/浅色模式自动切换。
6. 实战封装:下拉刷新与上拉加载更多
这是移动端列表的标配功能。虽然有一些第三方库,但自己封装一次能极大地加深对Compose状态、副作用和自定义布局的理解。我们将基于LazyColumn和SwipeRefresh(来自accompanist-swiperefresh,但请注意,Accompanist库已逐步迁移到官方库,这里用其原理讲解)的思想,封装一个通用的列表组件。
6.1 设计状态与参数
首先,定义组件需要的数据和回调接口。
// 列表状态 data class PagingListState<T>( val items: List<T> = emptyList(), val isLoading: Boolean = false, // 是否正在加载更多(上拉) val isRefreshing: Boolean = false, // 是否正在刷新(下拉) val hasMore: Boolean = true, // 是否还有更多数据 val error: Throwable? = null // 错误信息 ) // 可组合函数的参数 @Composable fun <T : Any> PagingLazyColumn( state: PagingListState<T>, modifier: Modifier = Modifier, onRefresh: (() -> Unit)? = null, // 下拉刷新回调,为null则不启用下拉刷新 onLoadMore: (() -> Unit)? = null, // 上拉加载回调,为null则不启用上拉加载 key: ((T) -> Any)? = null, // 用于LazyColumn item的key itemContent: @Composable (T) -> Unit // 每个列表项的UI ) { // 实现... }6.2 实现下拉刷新
我们可以利用SwipeRefresh组件(从androidx.compose.material3:material3的1.1.0版本开始,官方已提供实验性支持,或者使用稳定的第三方库如androidx.compose.material:material中的旧版)。这里以理解原理为主,展示一个简化的自定义思路。
核心是监听LazyColumn的滚动状态。当列表在顶部且向下拖拽时,触发刷新。
@Composable fun <T : Any> PagingLazyColumn(...) { val listState = rememberLazyListState() // 判断是否在顶部 val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 } } // 一个简单的自定义刷新指示器(实际项目可用更精美的) Box(modifier) { LazyColumn(state = listState) { // 刷新指示器项 if (state.isRefreshing) { item { Box( modifier = Modifier .fillMaxWidth() .height(60.dp), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } } items(items = state.items, key = key) { item -> itemContent(item) } // 加载更多指示器项 if (state.isLoading && state.hasMore) { item { Box( modifier = Modifier .fillMaxWidth() .height(80.dp), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } } else if (!state.hasMore && state.items.isNotEmpty()) { item { Text( text = "没有更多数据了", modifier = Modifier .fillMaxWidth() .padding(16.dp), textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } // 监听滚动,触发加载更多(简化版,实际需防抖) LaunchedEffect(listState) { snapshotFlow { val layoutInfo = listState.layoutInfo val totalItems = layoutInfo.totalItemsCount val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 // 如果最后一个可见项是倒数第二个,且正在加载中,且还有更多数据 lastVisibleItem >= totalItems - 2 && !state.isLoading && state.hasMore } .distinctUntilChanged() .filter { it } .collect { onLoadMore?.invoke() } } // 处理下拉刷新手势(简化逻辑,实际应用建议用成熟组件) // 这里只是一个原理示意,通过判断手势和位置来触发 onRefresh // 更健壮的实现需要处理拖拽偏移量、释放动画等。 } }6.3 实现上拉加载更多
上拉加载更多的逻辑相对直接,就是监听列表是否滚动到了底部附近。我们在上面的代码中已经通过LaunchedEffect和snapshotFlow实现了。这里有几个关键点:
- 触发条件:通常判断最后一个可见项是否是列表的最后几项(例如倒数第2项)。
- 防抖与防重:必须确保在加载过程中不会重复触发
onLoadMore。我们通过检查!state.isLoading && state.hasMore来防止。 - 显示状态:在列表底部显示一个加载指示器(
CircularProgressIndicator)或“没有更多”的提示。
6.4 在ViewModel中整合逻辑
在ViewModel中,你需要管理分页状态、发起网络请求并更新状态。
class ArticleViewModel : ViewModel() { private val _pagingState = MutableStateFlow(PagingListState<Article>()) val pagingState: StateFlow<PagingListState<Article>> = _pagingState.asStateFlow() private var currentPage = 1 init { loadInitialData() } fun loadInitialData() { viewModelScope.launch { _pagingState.update { it.copy(isRefreshing = true, error = null) } try { val result = repository.fetchArticles(page = 1) _pagingState.update { it.copy( items = result.articles, isRefreshing = false, hasMore = result.hasMore ) } currentPage = 1 } catch (e: Exception) { _pagingState.update { it.copy(isRefreshing = false, error = e) } } } } fun loadMore() { if (_pagingState.value.isLoading || !_pagingState.value.hasMore) return viewModelScope.launch { _pagingState.update { it.copy(isLoading = true) } try { val nextPage = currentPage + 1 val result = repository.fetchArticles(page = nextPage) _pagingState.update { oldState -> oldState.copy( items = oldState.items + result.articles, // 拼接列表 isLoading = false, hasMore = result.hasMore ) } currentPage = nextPage } catch (e: Exception) { _pagingState.update { it.copy(isLoading = false, error = e) } } } } fun refresh() { loadInitialData() // 刷新通常就是重新加载第一页 } }6.5 在Compose Screen中使用
最后,在界面中连接起来。
@Composable fun ArticleScreen(viewModel: ArticleViewModel = viewModel()) { val listState by viewModel.pagingState.collectAsStateWithLifecycle() // 处理错误(简单示例) listState.error?.let { error -> LaunchedEffect(error) { // 显示Snackbar等错误提示 scaffoldState.snackbarHostState.showSnackbar("加载失败: ${error.message}") } } PagingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), onRefresh = { viewModel.refresh() }, onLoadMore = { viewModel.loadMore() }, key = { it.id } // 假设Article有唯一id ) { article -> ArticleItem(article) // 你的列表项UI组件 } }实操心得与避坑指南:
- Key的重要性:务必为
LazyColumn的items函数提供稳定的key。这能保证在列表数据更新(如刷新、加载更多)时,Compose能高效地进行差异化比较,避免不必要的重组和状态丢失(比如输入框内容)。- 状态管理:
isLoading和isRefreshing要分开管理。下拉刷新是重置列表,而上拉加载是追加列表。逻辑混淆会导致数据错乱。- 防抖:上拉加载的监听事件流一定要加
.distinctUntilChanged()和.filter { it },并且结合isLoading状态判断,否则在快速滚动时可能触发多次加载。- 空状态与错误状态:不要忘记处理列表为空、加载失败等情况,提供友好的用户界面。
- 使用成熟库:对于生产环境,如果不想重复造轮子,可以考虑使用
androidx.compose.foundation中官方的pullRefresh修饰符(在较新版本中)或经过社区检验的第三方分页库(如Paging 3与Compose的集成),它们处理了更多边界情况和动画细节。
7. 常见问题与性能优化
在实际使用Compose的过程中,你会遇到一些典型问题。这里记录几个我踩过的坑和解决方案。
7.1 性能问题:不必要的重组
这是Compose新手最常遇到的问题。重组是高效的,但无谓的重组会浪费资源。
- 问题现象:一个
Composable函数频繁被调用,导致UI卡顿或逻辑重复执行。 - 排查工具:使用Android Studio的Layout Inspector中的Compose标签,或者开启重组计数(通过
-P androidx.compose.compiler.plugin.enableMetricsApi=true编译参数),可以可视化看到重组范围。 - 常见原因与解决:
- 将不稳定的参数传递给了
Composable:如果参数是每次调用父函数都会新创建的List或Data Class,且没有使用remember,Compose会认为它变了,导致重组。解决方案:使用remember缓存对象,或者确保数据类是稳定的(所有属性val且类型稳定)。 - 在
Composable函数中执行副作用:例如,在Composable函数体内直接发起网络请求、读写数据库。这些操作应该在LaunchedEffect、DisposableEffect或ViewModel中执行。 - 状态提升不足:状态定义在了过低的位置,导致其变化引起大范围重组。应将状态提升到足够高的、真正需要它的最近共同祖先。
- 使用了
derivedStateOf但依赖了不稳定的状态:derivedStateOf用于从其他状态派生出新状态。如果其依赖的状态本身不稳定,会导致派生状态频繁计算。确保依赖的状态是remember或mutableStateOf创建的。
- 将不稳定的参数传递给了
7.2 列表项内容闪烁或状态丢失
- 问题:在
LazyColumn中,滚动时列表项内的输入框内容可能会闪烁或重置。 - 原因:重组时,Compose无法正确识别哪些项是之前就存在的。如果列表数据变化(即使是追加),没有提供
key,Compose可能会错误地复用或丢弃节点。 - 解决:始终为
LazyColumn的items或itemsIndexed函数提供key参数。key应该是列表项数据的唯一标识符,如ID。items(items = state.items, key = { item -> item.id }) { item -> // ... }
7.3 与传统View的互操作
在迁移过程中,难免会遇到需要嵌入传统View(如WebView,MapView)的情况。
- 使用
AndroidView:AndroidView是一个Composable函数,它提供了一个工厂函数来创建和更新传统View。@Composable fun CustomWebView(url: String) { AndroidView( factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true } }, update = { webView -> webView.loadUrl(url) // 当url参数变化时,会调用update块 } ) } - 注意事项:
AndroidView内部管理的View生命周期与Compose组合生命周期绑定。在update块中处理状态更新,避免在factory中做重复初始化。对于复杂的View,可能需要使用DisposableEffect来清理资源。
7.4 导航(Navigation)
对于多页面应用,推荐使用Jetpack Navigation组件对Compose的支持。
- 添加依赖:
androidx.navigation:navigation-compose - 定义导航图:
@Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen(navController) } composable("detail/{itemId}") { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailScreen(itemId = itemId, navController) } // ... 其他目的地 } } - 执行导航:
// 在 HomeScreen 中 Button(onClick = { navController.navigate("detail/$itemId") }) { Text("查看详情") } - 传递复杂对象:不建议通过导航参数传递复杂对象。应该只传递最小标识符(如ID),在目标屏幕的
ViewModel中根据ID获取完整数据。这能更好地处理配置变更(如屏幕旋转)和进程死亡恢复。
从零上手Jetpack Compose,最大的障碍往往是思维模式的转变。一旦你接受了声明式UI和状态驱动的思想,并熟悉了基础组件和状态管理,就会发现开发效率有了质的飞跃。开始时可以从项目中的一个简单页面或组件入手,比如一个设置页面或一个商品卡片,逐步积累信心。遇到问题多查阅官方文档和社区样例,Compose的生态已经非常丰富。记住,实践是最好的老师,动手写起来,那些抽象的概念会很快变得具体。