009 结合 FirstComposeApp 项目深入学习案例
这篇文章是基于我自己的项目 D:\dazhao\androidStudio\FirstComposeApp 来写的。我发现,当学习不再停留在抽象概念,而是直接对着自己写的代码动手改造时,理解会深刻得多。所以,我打算采用一种 逐步重构 的方式来学习,建议你和我一样,边看边改,每完成一个案例就运行一次,亲眼看看变化。
1. 先看看我的项目现状
目前这个项目已经有了一个非常适合深入学习 Compose 的雏形,各个页面都承载了不同的学习点:
MainActivity.kt:目前还是模板工程默认的Hello Android,一个完美的“新手村”。LoginActivity.kt:非常适合用来学习表单输入、状态管理和校验逻辑。PlatformChannelActivity.kt:可以学习双输入框的表单处理和参数提交。TalkieIdSettingActivity.kt:适合研究单字段表单、顶部栏以及 Preview 的用法。SettingsScreen.kt:这里能学到Scaffold、底部导航、列表和弹窗的综合运用。SatelliteChatActivity.kt:是学习动画、状态驱动 UI 和副作用(Side Effects)的好材料。ui/theme/Theme.kt:从这里可以切入 Material3 主题体系的学习。
同时,我也刻意保留了一些“不完美”的地方,这些都是绝佳的练习素材:
- 入口页和其他页面之间还没有形成统一的 Compose 导航结构,显得有些割裂。
SettingsScreen.kt中,外层用了Column + verticalScroll,内层又嵌套了LazyColumn,这是个典型的布局问题。TalkieIdSettingActivity.kt的 Preview 函数调错了页面,正好可以学习如何正确组织预览。nav_graph.xml还是旧的 Fragment 导航思路,但项目主体已经转向 Compose Activity 了。SatelliteChatActivity.kt中有一部分演示逻辑直接写在了 Composable 函数里,适合进一步做业务逻辑的拆分。
所以,这个项目特别适合我们采用一种务实的学习方式:不是推倒重来,而是针对现有页面,一屏一屏地升级和优化。
2. 我建议的学习顺序
为了循序渐进,我给自己规划了三个阶段:
第一阶段:先把基础 UI 和状态管理吃透
- 从
LoginActivity.kt开始,夯实表单和状态基础。 - 接着是
PlatformChannelActivity.kt,处理多字段交互。 - 然后是
TalkieIdSettingActivity.kt,完善单字段场景。
第二阶段:开始学习页面结构与列表
- 深入
SettingsScreen.kt,理解复杂页面的骨架。 - 改造
MainActivity.kt,让它成为真正的应用入口。
第三阶段:学习 Compose 的进阶能力
- 钻研
SatelliteChatActivity.kt,玩转动画和高级状态管理。 - 最后,统一导航、主题和状态流,让应用架构更清晰。
案例 1:把 LoginActivity.kt 练成标准 Compose 表单页
当前我已经掌握了什么
目前这个页面已经有了:
- remember 和 mutableStateOf 管理状态。
- OutlinedTextField 和 Button 的基本使用。
- 用 Toast 做简单反馈。
- 基本的条件判断逻辑。
这说明我已经成功跨进了 Compose 的大门,处于“能用”的阶段。
我的下一步学习目标
目标是把当前这个“能跑起来”的页面,升级成一个“结构清晰、便于维护和扩展”的页面。这就像把毛坯房装修成精装房,功能不变,但体验和品质提升了。
我计划的重构点
- 拆分组件:把庞大的
LoginScreen()函数拆开。LoginScreen()作为容器,负责状态管理和组合。LoginForm(...)只负责渲染输入框。LoginAction(...)或直接保留一个清晰的按钮区域。
- 状态提升:这是 Compose 的核心思想之一。
- 让父组件(如
LoginScreen)持有username、password等状态。 - 子组件(如
LoginForm)变为无状态的,只接收数据和事件回调。
- 让父组件(如
- 增强交互:
- 增加按钮禁用逻辑:当用户名或密码为空时,保存按钮应不可点击。
- 增加更友好的错误提示:比如在输入框下方提示“用户名长度不足”或“密码太短”。
我能从中学到的 Compose 知识点
- 什么是有状态(Stateful)和无状态(Stateless)的 Composable。
- “状态提升”(State Hoisting)的意义和具体做法。
- 为什么组件拆小后,
@Preview会变得更容易编写和测试。 - 如何把表单页面写得像“声明UI”,而不是传统“拼控件”的思路。
我的练习建议
可以把目前分散的状态变量:
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
提升为一个统一的数据结构,例如:
data class LoginUiState(
val username: String = "",
val password: String = "",
val loading: Boolean = false,
val errorMessage: String? = null
)
这样做能为后续引入 ViewModel 铺平道路。
进一步升级的思路
当我把本页按照上述思路改造完后,很自然地就能接着学习下面3个东西,让页面更完整:
- 使用 collectAsState() 来收集 ViewModel 中的 StateFlow 状态。
- 为登录按钮添加 loading 状态(比如显示进度条)。
- 登录成功后,如何进行导航跳转。
案例 2:把 PlatformChannelActivity.kt 和 TalkieIdSettingActivity.kt 练成“可复用表单页”
这两个页面简直是绝配,非常适合放在一起学习。因为它们本质上都是“输入一些配置信息然后保存”的场景。
这两个页面在当前项目中的价值
PlatformChannelActivity.kt:代表双字段表单。TalkieIdSettingActivity.kt:代表单字段表单。
这正好可以训练我把“具体的业务页面”抽象成“通用的表单模式”的能力。
我设定的学习目标
- 抽取公共结构:观察两个页面,找出共同部分。
- 顶部的
TopAppBar(返回按钮和标题)。 - 中间的输入区域(一个或多个字段)。
- 底部的保存按钮。
- 顶部的
- 分离关注点:学会把表单的校验逻辑从按钮的
onClick事件里剥离出来。 - 掌握输入框细节:
- 学习使用
KeyboardOptions(比如限制数字键盘)。 - 为输入框补充更多 Material Design 元素:
isError(错误状态)supportingText(辅助文本/错误提示)leadingIcon/trailingIcon(前后图标)
- 学习使用
项目里一个很适合立即修正的问题
我注意到 TalkieIdSettingActivity.kt 文件底部的 Preview 写成了这样:
@Preview(showBackground = true)
@Composable
fun TalkieIdSettingScreen() {
MyApplicationTheme {
PlatformChannelSettingScreen( // 注意!这里预览的是另一个页面!
onBack = {},
onConfigure = { _, _ -> }
)
}
}
这个问题的典型性在于:
- Preview 函数名是 TalkieIdSettingScreen,但实际渲染的却是 PlatformChannelSettingScreen 组件。
- 这提醒我:Preview 应该尽量做到一页一个,并且命名清晰,避免混淆。
我的练习建议
我可以尝试创建两个更基础、更可复用的无状态组件:
- SettingTextField(...):封装了标签、错误提示等逻辑的输入框。
- SettingSaveButton(...):封装了禁用状态等逻辑的按钮。
然后用这两个基础组件来重构那两个页面。这个过程会让我深入思考:
- 组件的抽取边界在哪里?
- 一个可复用的 Composable,它的参数列表应该如何设计?
- 预览函数在项目中应该如何组织和管理?
案例 3:用 SettingsScreen.kt 学真正的页面结构与列表
这个页面是我项目里目前最复杂、也最值得深入打磨的一页。因为它已经不再是简单的控件堆砌,而是包含了:
- Scaffold(应用骨架)
- 底部导航栏
- 用户信息头部卡片
- 设置项列表
- AlertDialog(弹窗)
它非常接近一个真实的业务页面了。
当前页面最值得我学习的点
SettingItem 数据模型 + SettingList + SettingRow 这个结构,已经初具“声明式列表渲染”的雏形了。数据驱动UI的思想在这里有了很好的体现。
当前页面最值得我修正的问题
我现在外层写了一个 Column 并设置了 verticalScroll,而内层的 SettingList 函数里又用了一个 LazyColumn:
@Composable
private fun SettingList(items: List<SettingItem>) {
LazyColumn( // 内层滚动
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(items) { item ->
SettingRow(item = item)
}
}
}
// 而在 SettingsScreen() 中:
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()) // 外层滚动
.padding(innerPadding)
) { ... }
这会导致一个经典的 Compose 布局问题:外层和内层同时可以纵向滚动,体验非常奇怪。
我打算怎么改
最好的办法是把整个页面改造成一个单一的 LazyColumn,把头部卡片和列表项都作为它的子项。例如:
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(vertical = 16.dp)
) {
item {
UserHeaderCard() // 头部作为单独一项
}
items(settingsList, key = { it.id }) { item -> // 列表项
SettingRow(item = item)
}
}
我能从中学到的 Compose 点
- 为什么复杂的、需要滚动的页面应该优先使用单一的滚动容器。
LazyColumn不只是用来做纯列表,它可以作为整个页面的布局骨架。key = { it.id }参数对于列表性能优化的重要性。- 在一个
LazyColumn中,如何和谐地共存头部、列表项和可能出现的弹窗等元素。
进阶练习想法
- 为不同的设置项(如“通知”、“隐私”)配上不同的图标,而不是都用
Icons.Default.Person。 - 实现点击“对讲机ID设置”项时,能跳转到
TalkieIdSettingActivity。 - 实现点击“平台通道设置”项时,能跳转到
PlatformChannelActivity。 - 把“版本号”的文本内容改成动态获取自
BuildConfig.VERSION_NAME。
案例 4:把 MainActivity.kt 从模板页升级为项目入口
目前的 MainActivity.kt 还是 Android Studio 生成的模板代码:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
这页最适合用来学习 一个 Compose 工程的入口应该怎么组织。
我设定的学习目标
把 MainActivity 升级为整个 App 的 Compose 总入口,让它承担起更重要的职责:
- 统一包裹 MyApplicationTheme,确保全局主题一致。
- 放置整个应用的 NavHost(导航宿主)。
- 决定应用的首页是登录页还是直接进入主设置页。
- 逐步替代掉那些零散的、独立的 Activity 入口。
我的练习建议
一开始可以不用做太复杂的导航图,先做一个“案例菜单页”试试水。在这个菜单页里,用列表列出所有我们已经写好的页面:
- 登录页
- 对讲机 ID 设置页
- 平台通道设置页
- 设置页
- 卫星聊天页
点击每一项就跳转到对应的页面。这个简单的练习能让我快速理解在 Compose 中,“一个容器页如何组织和管理多个子页面”的基本思想。
我能从中学到的 Compose 点
- 如何使用
Scaffold来组织一个页面的顶层结构。 - Compose 中页面间导航的基本思想。
- 为什么现代 Android 开发中,Compose 更适合与单 Activity 架构结合。
案例 5:结合 nav_graph.xml 学“从 XML 导航迁移到 Navigation Compose”
我的项目里现在还保留着一个 nav_graph.xml 文件,这说明我的一部分思路还停留在旧的 XML Navigation 模式上。
但从现状看,项目的主要页面都已经用 Compose 写了,所以现在正是开始迁移到 navigation-compose 库的绝佳时机。
学习目标
通过这个案例,我可以做一次清晰的思路对照:
1. 旧思路:
- 在 nav_graph.xml 中定义目的地(Destination)和动作(Action)。
- 目的地是 Fragment。
- 在代码中使用 findNavController() 进行导航。
2. 新思路:
- 在 Kotlin 代码中直接定义 NavHost。
- 使用 composable(route = "...") { ... } 来声明可组合目的地。
- 使用 navController.navigate(...) 进行导航。
我计划在这个项目里怎么落地
不用一步到位。可以先为最核心的3个页面建立路由(Route):
- login(登录页)
- settings(设置页)
- satellite_chat(卫星聊天页)
然后在改造好的 MainActivity 中放置一个 NavHost,并设置 settings 为起始目的地。
为什么这个案例特别适合现阶段的我
因为我正处在一个非常典型的“进阶”阶段:
- 已经学会了如何编写独立的 Compose 页面。
- 但这些页面还是分散在多个 Activity 中,像一个个孤岛。
- 还没有真正把这些页面组织成一个有机的、完整的 Compose 应用。
掌握 Navigation Compose,正是从“会写 Compose 控件”跨越到“会搭建 Compose 应用架构”的关键分水岭。
案例 6:用 SatelliteChatActivity.kt 学动画、状态机和副作用
这是我项目里最有潜力、也最像真实产品原型的一页。它已经包含了很多 Compose 的进阶能力:
- rememberInfiniteTransition(无限动画)
- LaunchedEffect(副作用)
- Canvas(自绘)
- 自定义的视觉效果(如信号波纹)。
- 复杂的按压交互逻辑。
- 根据状态(连接中、通话中、发送成功/失败)切换不同的 UI。
这一页我建议重点学什么
1. 把 UI 状态和演示逻辑分开
目前页面里有一些用于演示的逻辑,比如:
- 每5秒自动切换一次消息发送状态。
- 松开通话按钮后,随机决定成功或失败。
在初学阶段这样写没问题,但下一步我建议改为更清晰的结构:
- 定义一个 SatelliteChatUiState 数据类来集中管理所有状态。
- 定义明确的事件函数,如 onPressTalk()、onReleaseTalk()。
- 让所有的状态变化都通过响应这些事件来集中管理。
2. 学会识别哪些逻辑属于“副作用”
比如:
- 定时器(每5秒刷新状态)。
- 发起真实的网络请求来获取连接状态。
- 开始或结束录音。
这些逻辑都不应该散落在 UI 组件的内部,而应该被提取到 ViewModel 中,或者通过 LaunchedEffect、rememberCoroutineScope 等 Compose 副作用 API 来妥善管理。
3. 深入学习手势输入和“按住说话”交互
TalkButtonArea() 里已经用到了 pointerInput。这是一个非常好的起点,可以继续深入:
- 按下按钮时开始录音(或模拟开始)。
- 抬起手指时结束并发送。
- 手指滑出按钮区域时取消发送。
- 增加长按阈值判断。
- 处理防止误触的逻辑。
这些都是业务中非常常见的交互需求,在 Compose 里用 pointerInput 可以很优雅地实现。
4. 实践更精细的组件拆分
当前页面已经初步拆成了几个大组件:
- SatelliteConnectionArea
- MessageStatusArea
- TalkButtonArea
方向是对的。下一步可以继续把每个大组件内部的不同状态(比如成功态、失败态、发送中态)拆分成更小、更专注的状态组件。
我能从中学到的 Compose 点
- 动画如何与具体的业务状态进行绑定和驱动。
LaunchedEffect适合在什么场景下使用(生命周期、条件触发)。- 面对一个复杂的页面,如何有条理地将其拆分成多个可组合函数。
- 在 Compose 中,实现自定义视觉效果不一定需要回到 XML 写
Drawable,直接用Canvas绘制也很方便。
案例 7:统一主题与设计语言
我注意到项目中目前有两套主题思路:
- ui/theme/Theme.kt 中定义的 MyApplicationTheme(全局主题)。
- SatelliteChatActivity.kt 文件底部单独定义的 SatelliteAppTheme(页面局部主题)。
这其实也是一个很好的学习案例,关于如何管理应用的设计系统。
我设定的学习目标
学习如何将主题合理地分为两层:
1. 全局 App Theme:定义品牌色、字体、形状等基础设计令牌(Design Tokens)。
2. 业务页面局部扩展:在全局主题的基础上,为特定页面(如深色模式的聊天页)进行微调。
我的推荐做法
- 全局统一使用 `MyApplication
当前文章价值3.29元,扫一扫支付后添加微信提供帮助!(如不能解决您的问题,可以申请退款)

评论已关闭!