跳到正文Lab Week 2 Summary · Jimmy Huang's BlogLocalContext.current
搭配
Toast.makeText(...)
做点击反馈
能完成最终的 Seasons and Months 小应用
Task 0: 创建项目并确认 Compose 入口
新建一个 Android Compose 项目 LayoutLab
打开 MainActivity.kt,后续主要修改这个文件
确认 setContent { ... } 是界面入口
确认 @Preview 可以用来看布局预览
确认 Android Studio 的导入和 Preview 面板能正常工作
Step 0.1:创建 LayoutLab 项目
在 Android Studio 中创建一个新的 Empty Activity 项目,项目名填 LayoutLab,语言选 Kotlin。创建完成后,等 Gradle Sync 跑完再开始修改代码。
先拿到一个带 Jetpack Compose 模板的工程,后面所有 Task 都在这个工程里完成。
如果你的包名不是老师截图里的示例包名,不需要改,保持你自己的包名即可。
如果你的主题名不是 LayoutLabTheme,下面所有代码里的主题名都换成项目自动生成的那个。
本周最省事的做法是先把所有 composable 都写在 MainActivity.kt 里,确认跑通后再考虑拆文件。
Step 0.2:确认 setContent 是应用入口
打开 MainActivity.kt,你会看到类似下面这种结构:
setContent 是 Compose 画界面的入口。你后面每做一个 Task,本质上都是把这里“当前正在显示的 composable”换掉。
本周 Lab 要你不断切换不同的 composable 来看效果,所以最核心的入口就是 setContent。
Step 0.3:确认 Preview 和自动导入可用
打开 MainActivity.kt 后,点击右上角的 Split 或 Design,确认右侧能看到 Compose Preview。
Preview 用来在不运行模拟器的情况下快速看布局效果。
本周前两个基础布局任务用 Preview 看最方便;但真正涉及点击和 Toast 的部分,还是要跑模拟器或真机。
如果类名标红,按 Alt + Enter 自动导入
也可以在 Android Studio 里开启 Auto Import
如果 Preview 没更新,先 Build Project 再点刷新
Task 1: Column Layout
新建一个 ColumnLayoutCompose
用 Column 把四季从上到下排出来
更新 onCreate 和 Preview,让应用显示这个 composable
再用 Surface 和 padding 改善 UI
这是老师会检查的第一个基础点:你是否真的理解垂直布局
Step 1.1:创建 ColumnLayoutCompose
先在 MainActivity.kt 里新增下面这个 composable:
这里用 Column 做了一个最基础的垂直布局,把四个 Text 从上到下排列。
Column 是 Compose 里最基础的纵向布局容器。只要子元素写在里面,它们就会按顺序往下排。
如果成功预览,你会看到四个季节垂直显示,默认会比较朴素,没有额外背景和间距。
Step 1.2:更新 setContent 和 Preview
把 setContent 中原本默认显示的内容改成 ColumnLayoutCompose(),并新增一个 Preview:
把应用入口和预览入口都切到你刚写的 ColumnLayoutCompose。
只写 composable 还不够;你必须告诉应用“现在要显示谁”,Preview 才知道应该渲染哪个函数。
看 Preview:应该能看到四季纵向排列
点 Run:模拟器里也应该显示同样内容
Step 1.3:用 Surface 和 padding 改善 UI
把刚才的 ColumnLayoutCompose 替换成下面这个版本:
给原本很朴素的四个季节加一个 Material 风格的背景容器,并在四周留出 24.dp 的空间。
Preview 里会看到一个带主题色背景的小区域,四个季节仍然是纵向排列,但整体更像一个完整的组件。
Task 2: Row Layout
Step 2.1:创建 RowLayoutCompose
在 MainActivity.kt 里新增下面这个 composable:
Row 是 Compose 里最基础的横向布局容器,适合做一行内容的排布。
你会看到四个季节横向排成一行,并尽量分散在整行宽度里。
Step 2.2:更新 setContent 和 Preview
把当前入口改成 RowLayoutCompose:
Preview 中应该看到一条横向排列的四季文字
运行到模拟器中时,也应显示相同布局
Task 3: Nested Layouts, Button and Toast
Step 3.1:创建 ButtonCompose
在 MainActivity.kt 里新增下面这个 composable:
Box 负责让整个内容块居中
Surface 负责背景
Column 负责把四季和按钮从上到下排
ElevatedButton 负责点击
Toast 负责反馈信息
如果只有 Column,内容默认会出现在左上角。这里需要“整块内容居中”,所以在外面套一个 Box,并用 contentAlignment = Alignment.Center 来统一定位。
val context = LocalContext.current:拿到 Android 的 Context,后面 Toast 要用
Spacer(modifier = Modifier.height(20.dp)):在四季文字和按钮之间加垂直间距
Toast.makeText(...).show():按钮点下去后显示一条短消息
界面中央会有一块带背景色的内容,里面纵向显示四季,底部有一个 Click 按钮。
android.widget.Toast
androidx.compose.ui.platform.LocalContext
androidx.compose.foundation.layout.Box
androidx.compose.foundation.layout.Spacer
androidx.compose.material3.ElevatedButton
Step 3.2:更新入口并在模拟器里验证 Toast
先看 Preview 确认布局没问题,再真正运行到模拟器或真机里测试点击效果。
Preview 更适合看静态布局,但 Toast 这种系统反馈,最好还是在真实运行环境里验证。
Task 4: Problem Solving — Seasons and Months
Step 4.1:把图片加入 drawable
先准备一张 license-free 图片。老师给的示例是一张四片叶子的图,你也可以用任何无版权问题的图片。把图片放进:
让 Compose 可以通过 R.drawable.seasons 读取这张本地图片资源。
最终任务明确要求界面中要有一张图片,所以这一步是必须的。
文件名只能用小写字母、数字和下划线,不要有空格和大写
Kotlin 代码里引用资源时不要写扩展名,写成 R.drawable.seasons 即可
如果你的文件名不是 seasons,后面的代码也要对应改掉
Step 4.2:实现最终的 SeasonsMonthsApp
下面这份代码可以直接作为最终 problem-solving 版本来完成本周任务:
这就是最终的小应用版本,它把本周所有重点合在一起:
Column 做整个页面的纵向主结构
Surface 做顶部标题样式
Image 读入本地图片
Row 放四个季节按钮
Toast 显示澳大利亚季节对应月份
题目要求你必须用 nested layouts,而且要有标题、图片、按钮和点击反馈。一个 Column 加一个内部 Row,就是最直接也最符合本周教学内容的写法。
标题:Surface + Text,做出有背景的标题块
图片:painterResource(id = R.drawable.seasons) 从 drawable 读取图片
按钮行:Row(..., horizontalArrangement = Arrangement.SpaceEvenly) 让按钮在整行里均匀分布
点击反馈:每个按钮用各自的 Toast 显示对应月份
Spring:September, October, November
Summer:December, January, February
Autumn:March, April, May
Winter:June, July, August
这里我用的是 Arrangement.SpaceEvenly,因为题目要求按钮要 equally spaced across the screen。
如果你的模拟器宽度比较小,按钮显得太挤,可以稍微减小图片高度,或者把按钮改成等宽布局。
如果你的图片文件名不是 seasons,一定要改 R.drawable.seasons 这一行。
Image
painterResource
ContentScale
RoundedCornerShape
FontWeight
sp
Toast
LocalContext
Step 4.3:把最终版本接到 MainActivity
把应用入口切到 SeasonsMonthsApp:
让应用启动时直接显示最终的 problem-solving 成品。
Step 4.4:按提交标准检查最终效果
这个 app 用 Column 做页面主结构,用 Row 放四个季节按钮,所以满足 nested layouts 的要求;标题用 Surface 做样式,图片从 drawable 读取;每个按钮点击后用 Toast 显示澳大利亚对应季节的三个月份。
Summary
这周真正要掌握的,不是把四季和月份硬背出来,而是理解 Compose 是怎么把 UI 一层层拼出来的。
你先用 Column 学会“从上往下排”,再用 Row 学会“从左往右排”,接着用 Box 理解“整块内容怎么定位到屏幕中央”,最后再把 Surface、Image、ElevatedButton、Toast 组合起来,做出一个真正有标题、有图片、有交互的小应用。
如果你现在能从一个空的 Compose 模板出发,自己写出:
本周必须理解的知识点
@ComposableCompose 里的 UI 不是写 XML,而是写 Kotlin 函数。只要一个函数负责“描述界面”,它通常就是一个 composable。
ModifierModifier 不是布局本身,而是给组件附加行为的链。像 padding、fillMaxWidth、fillMaxSize、height 都是通过 Modifier 来控制。
Column、Row、Box这三个是本周最核心的布局。以后很多复杂界面,本质上也是这几个布局嵌套出来的。
Surface它是 Material 风格容器,适合做背景、形状和内容承载。比单纯“刷个背景色”更贴近 Compose 的常见写法。
Toast 需要 ContextCompose 函数里不能直接凭空拿到 Android Context,所以要用 LocalContext.current。
Preview 不等于真实运行Preview 很适合快速看布局,但按钮点击、Toast、设备适配,最好还是在模拟器或真机上验证。
常见报错与排查
Unresolved reference / 类名标红先按 Alt + Enter 自动导入。常见缺失导入包括 Toast、LocalContext、dp、sp、painterResource、ContentScale。
Preview 不显示 / 一直转圈 / 显示旧内容先看项目是否还有编译错误。只要代码没完全编过,Preview 往往就不会正常刷新。先 Build Project,再点 Preview 刷新。
R.drawable.seasons 找不到检查图片是否真的放在 app/src/main/res/drawable/,文件名是否全小写,代码里是否没有写扩展名。
点击按钮后没有 Toast先确认你是在模拟器或真机里运行,不是只看 Preview。Toast 这种系统反馈最好在真实运行环境里验证。
Row 里的间距不对在 Row 里写 Spacer(height = ...) 不会产生横向间距,因为 height 影响的是垂直方向。
如果你需要横向间距,要用:
Spacer(width = ...)
Arrangement.SpaceBetween
Arrangement.SpaceEvenly
或者 weight(1f)
关键概念对比
Column vs Row vs Box
Preview vs 实际运行
老师可能会问什么
为什么 Task 1 里的 Greeting("Android") 要删掉?因为我们现在显示的是自己写的 composable,而不是模板默认的 Greeting。新的 composable 没有 String 参数,就不能再传 "Android"。
为什么 Task 3 里要把 Column 放进 Box?因为单独的 Column 默认会从左上角开始排。外层加 Box(fillMaxSize(), contentAlignment = Alignment.Center) 之后,整块内容就能放到屏幕中央。
为什么 Toast 要先拿 LocalContext.current?因为 Toast.makeText(...) 需要 Android Context,而 Compose 里常见的拿法就是 LocalContext.current。
为什么 final task 要用 nested layouts?因为一个布局不够同时处理“页面整体纵向结构”和“按钮横向分布”。最自然的做法就是外层 Column,里面再嵌一个 Row。
Surface 有什么用?它是 Material 风格容器,能帮你处理背景色、形状和内容承载。比直接在单个文本上硬改背景更符合 Compose 写法。
最后代码
app/src/main/res/drawable/
setContent {
LayoutLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
// 后面会把这里换成 ColumnLayoutCompose / RowLayoutCompose /
// ButtonCompose / SeasonsMonthsApp
}
}
}
@Composable
fun ColumnLayoutCompose(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "Spring")
Text(text = "Summer")
Text(text = "Autumn")
Text(text = "Winter")
}
}
setContent {
LayoutLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ColumnLayoutCompose(modifier = Modifier.padding(innerPadding))
}
}
}
@Preview(showBackground = true)
@Composable
fun ColumnLayoutPreview() {
LayoutLabTheme {
ColumnLayoutCompose()
}
}
@Composable
fun ColumnLayoutCompose(modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Column(modifier = modifier.padding(24.dp)) {
Text(text = "Spring")
Text(text = "Summer")
Text(text = "Autumn")
Text(text = "Winter")
}
}
}
@Composable
fun RowLayoutCompose(modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Spring")
Text(text = "Summer")
Text(text = "Autumn")
Text(text = "Winter")
}
}
}
setContent {
LayoutLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
RowLayoutCompose(modifier = Modifier.padding(innerPadding))
}
}
}
@Preview(showBackground = true)
@Composable
fun RowLayoutPreview() {
LayoutLabTheme {
RowLayoutCompose()
}
}
@Composable
fun ButtonCompose(modifier: Modifier = Modifier) {
val context = LocalContext.current
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Surface(color = MaterialTheme.colorScheme.primary) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Spring")
Text(text = "Summer")
Text(text = "Autumn")
Text(text = "Winter")
Spacer(modifier = Modifier.height(20.dp))
ElevatedButton(
onClick = {
Toast.makeText(
context,
"This is a Toast",
Toast.LENGTH_SHORT
).show()
}
) {
Text("Click")
}
}
}
}
}
setContent {
LayoutLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ButtonCompose(modifier = Modifier.padding(innerPadding))
}
}
}
@Preview(showBackground = true)
@Composable
fun ButtonComposePreview() {
LayoutLabTheme {
ButtonCompose()
}
}
setContent {
LayoutLabTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
SeasonsMonthsApp(modifier = Modifier.padding(innerPadding))
}
}
}
@Preview(showBackground = true)
@Composable
fun SeasonsMonthsAppPreview() {
LayoutLabTheme {
SeasonsMonthsApp()
}
}
@Composable
fun SeasonsMonthsApp(modifier: Modifier = Modifier) {
// 取得当前 Android 上下文,Toast 需要用它来显示提示信息
val context = LocalContext.current
// 外层总布局:整页内容从上到下排列
Column(
modifier = modifier
.fillMaxSize() // 让整个页面占满可用屏幕空间
.padding(horizontal = 16.dp, vertical = 24.dp), // 给整个页面四周留一点边距
horizontalAlignment = Alignment.CenterHorizontally // 让 Column 里的子组件默认水平居中
) {
// 标题外面的背景容器
Surface(
shape = RoundedCornerShape(20.dp), // 圆角矩形背景
color = MaterialTheme.colorScheme.primary // 使用主题主色作为背景色
) {
// 页面顶部标题文字
Text(
text = "Seasons and Months",
fontSize = 28.sp, // 字体大小
fontWeight = FontWeight.Bold, // 加粗
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp) // 标题文字内边距
)
}
// 标题和图片之间留出垂直空白
Spacer(modifier = Modifier.height(32.dp))
// 显示 drawable 目录中的 seasons 图片
Image(
painter = painterResource(id = R.drawable.seasons), // 加载本地图片资源
contentDescription = "Seasons image", // 无障碍描述
modifier = Modifier
.fillMaxWidth() // 图片宽度占满可用宽度
.height(220.dp), // 固定图片高度
contentScale = ContentScale.Crop // 按裁剪方式填满区域,避免留白
)
// 图片和按钮区域之间留出一点空白
Spacer(modifier = Modifier.height(16.dp))
// 四个季节按钮横向排列,并尽量均匀分布
Row(
modifier = Modifier.fillMaxWidth(), // 让这一行占满宽度,方便做“等距分布”
horizontalArrangement = Arrangement.SpaceEvenly // 按钮之间以及两侧尽量平均分配空间
) {
// 春天按钮
ElevatedButton(
onClick = {
// 点击按钮后弹出 Toast,显示澳大利亚春季的三个月
Toast.makeText(
context,
"September, October, November",
Toast.LENGTH_SHORT
).show()
}
) {
Text("Spring")
}
// 夏天按钮
ElevatedButton(
onClick = {
// 点击按钮后弹出 Toast,显示澳大利亚夏季的三个月
Toast.makeText(
context,
"December, January, February",
Toast.LENGTH_SHORT
).show()
}
) {
Text("Summer")
}
// 秋天按钮
ElevatedButton(
onClick = {
// 点击按钮后弹出 Toast,显示澳大利亚秋季的三个月
Toast.makeText(
context,
"March, April, May",
Toast.LENGTH_SHORT
).show()
}
) {
Text("Autumn")
}
// 冬天按钮
ElevatedButton(
onClick = {
// 点击按钮后弹出 Toast,显示澳大利亚冬季的三个月
Toast.makeText(
context,
"June, July, August",
Toast.LENGTH_SHORT
).show()
}
) {
Text("Winter")
}
}
}
}