第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器

第 4 周 Unit 2:Jetpack Compose 状态、按钮、计数器与小费计算器

学习主题:Jetpack Compose 基础 --- 状态管理、按钮交互、计数器与小费计算器

建议时长:5 天

学习目标:理解 Compose 的声明式 UI 思想,掌握 remember / mutableStateOf 状态管理,能独立完成 Dice Roller 和 Tip Calculator 两个应用

本文适合已有 Kotlin 基础、刚接触 Jetpack Compose 的 Android 初学者。

如果你在第 3 周用 XML Views 写过生日卡和单位转换器,本周将过渡到 Compose 构建界面------不需要再写 XML 布局,所有 UI 代码都用 Kotlin 完成。

阅读前需要:Android Studio Hedgehog(2023.1.1)或更新版本,会写 Kotlin 函数和变量。


一、学习前检查清单

检查项 回答
读者对象 已学完 Kotlin 基础 + Views 入门,开始学 Compose 的 Android 初学者
文章目标 理解 Compose 声明式 UI 和状态管理,完成计数器、骰子、小费计算器三个应用
技术关键词 Jetpack Compose、remember、mutableStateOf、Button、TextField、Dice Roller、Tip Calculator
内容边界 仅限 Compose Unit 2:状态、按钮、计数器、随机骰子、小费计算

二、Day 1:Jetpack Compose 入门 --- 从 Views 到 Compose

1. 为什么学 Compose

第 3 周我们用 XML 写布局(activity_main.xml)+ findViewById 获取控件。这种方式有几个痛点:

  • XML 和 Kotlin 代码分离,需要不断在两个文件间切换
  • findViewById 容易写错 ID,运行时才报错
  • 控件多的时候代码冗长

Jetpack Compose 是 Android 的现代化 UI 工具包,核心思想是声明式 UI

text 复制代码
传统 Views(命令式):你先创建一个 TextView,然后一步步教它怎么做(setText、setColor...)
Compose(声明式):你直接描述界面应该长什么样,数据变了界面自动刷新

2. 创建第一个 Compose 项目

环境说明(我的开发环境):

  • Android Studio Hedgehog | 2023.1.1
  • Kotlin 1.9.x
  • Compose BOM 2024.01.00
  • Gradle 8.x
  • 模拟器:Pixel 6 API 34

步骤:

  1. Android Studio → New Project → 选择 Empty Activity(注意:不是 Empty Views Activity)
  2. Project Name:ComposeCounter
  3. Language:Kotlin
  4. Minimum SDK:API 24
  5. 点击 Finish,等待 Sync 完成

项目结构对比:

text 复制代码
Views 项目(第 3 周)              Compose 项目(本周)
├── activity_main.xml              ├── MainActivity.kt(包含 UI 代码)
├── MainActivity.kt                └── ui/theme/(主题配置)
├── strings.xml
└── drawable/

没有 XML 布局文件了------所有界面都在 Kotlin 文件中用 @Composable 函数描述。

3. 第一个 Composable 函数

打开 MainActivity.kt,默认代码:

kotlin 复制代码
package com.example.composecounter

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 这里写 UI 代码
            Greeting("Android")
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

说明:

  • setContent {}:Compose 的入口,替代了 setContentView(R.layout.activity_main)
  • @Composable:标注这是一个描述界面的函数,所有 UI 函数都要加这个注解
  • Text():Compose 的文本组件,相当于 View 里的 TextView
  • Greeting("Android"):调用 Composable 函数,传参数进去

运行效果:

text 复制代码
Hello Android!

4. Composable 函数的规则

kotlin 复制代码
// ✅ 正确
@Composable
fun MyScreen() {
    Text("Hello")           // 函数内部可以调用其他 @Composable 函数
}

// ✅ 正确 --- 可以带参数
@Composable
fun MyScreen(title: String) {
    Text(title)
}

// ❌ 错误 --- 没有 @Composable 的函数里不能调用 Composable
fun regularFunction() {
    Text("Hello")           // 编译报错!
}

核心规则:

  • @Composable 函数只能被另一个 @Composable 函数调用
  • setContent {} 是唯一的例外------它是系统提供的 Compose 入口桥接
  • Composable 函数可以带参数,没有返回值(返回 Unit

5. Day 1 练习任务

  1. 创建一个 Empty Activity 项目,修改 Greeting 函数的文本为自己的欢迎语
  2. setContent {} 中调用两次 Greeting,给不同参数,观察运行结果
  3. 试着在 Greeting 函数中再添加一个 Text(),显示第二行文字

三、Day 2:状态(State)与按钮(Button)--- 计数器应用

1. 为什么学这个

第 3 周的单位转换器已经涉及"按钮点击 → 执行计算 → 更新界面"。但当时你需要手动写 findViewByIdsetOnClickListener,然后手动更新 TextView.text

Compose 的核心理念是:界面是状态的函数。状态变了,界面自动刷新。你不需要手动更新 UI,只需要更新数据。

2. 回忆(记忆)与可变状态

kotlin 复制代码
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
kotlin 复制代码
@Composable
fun CounterScreen() {
    // remember:让变量在界面刷新时"记住"上一次的值
    // mutableStateOf:标记这个变量是"状态",变了就会自动刷新界面
    var count by remember { mutableStateOf(0) }

    Text(text = "当前计数:$count")
}

说明:

关键字 作用 类比
remember 保存值,防止每次刷新时重置为初始值 记笔记------刷新后还记得上次写的内容
mutableStateOf 创建一个"可观察"的状态容器,值变化时通知 Compose 重新绘制 报警器------值变了自动拉响警报,触发界面重绘
by(属性委托) count 用起来就像一个普通 Int 变量,读写时自动操作 State 对象 快捷方式------省去写 .value 的麻烦

等价写法(不用 by):

kotlin 复制代码
val countState = remember { mutableStateOf(0) }
// 读取:countState.value
// 修改:countState.value = 1

Text(text = "当前计数:${countState.value}")

使用 by 委托更简洁,推荐。

3. 按钮:Button 与点击事件

kotlin 复制代码
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    // Button 相当于 View 里的 Button 控件
    Button(onClick = {
        // 点击时执行:count 自增
        count++
    }) {
        Text("点我 +1")   // 按钮上的文字
    }

    Text(text = "当前计数:$count")
}

说明:

  • Button(onClick = { ... }):Compose 的按钮,onClick 是点击回调
  • count++:修改状态变量,Compose 自动重新绘制所有用到 count 的 UI
  • 不需要 findViewById、不需要 setOnClickListener、不需要手动更新 TextView

4. 完整计数器应用

kotlin 复制代码
package com.example.composecounter

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CounterScreen()
        }
    }
}

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }

    // Column:竖直排列子组件(相当于 LinearLayout vertical)
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "当前计数",
            fontSize = 20.sp
        )

        Spacer(modifier = Modifier.height(8.dp))

        Text(
            text = "$count",
            fontSize = 48.sp
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = { count++ }) {
            Text("点我 +1")
        }

        Spacer(modifier = Modifier.height(8.dp))

        Button(onClick = { count = 0 }) {
            Text("重置")
        }
    }
}

逐段说明:

代码 说明
Column(...) 竖直排列布局,替代 Views 中的 LinearLayout(vertical)
Modifier.fillMaxSize() 占满整个屏幕(相当于 match_parent
verticalArrangement = Arrangement.Center 子组件在垂直方向居中
horizontalAlignment = Alignment.CenterHorizontally 子组件在水平方向居中
Spacer(modifier = Modifier.height(8.dp)) 空白间距,8dp 高(相当于 layout_margin
.sp.dp Compose 的尺寸单位,.dp 用于尺寸,.sp 用于文字

运行效果:

text 复制代码
┌──────────────────────┐
│                      │
│       当前计数        │
│                      │
│         5            │   ← 点击按钮会变
│                      │
│   ┌──────────┐      │
│   │ 点我 +1   │      │
│   └──────────┘      │
│   ┌──────────┐      │
│   │   重置    │      │
│   └──────────┘      │
│                      │
└──────────────────────┘

5. 状态工作的关键理解

错误写法:

kotlin 复制代码
@Composable
fun CounterScreen() {
    var count = 0   // ❌ 没有 remember + mutableStateOf

    Button(onClick = { count++ }) {
        Text("+1")
    }
    Text("$count")  // 点击按钮后界面不会更新!
}

为什么界面不更新 :Compose 只会为用 mutableStateOf 标记的变量触发重绘。普通 var 变量的变化不会被 Compose 感知。

6. 常见错误

错误写法 原因 正确写法
var count = 0 然后在 Button 里 count++ 没有用 mutableStateOf,Compose 不知道数据变了 var count by remember { mutableStateOf(0) }
val count by remember { mutableStateOf(0) } val 不能重新赋值 var
忘记 remember 每次刷新重置为初始值 0 remember { mutableStateOf(0) }
忘记 import 语句 by 委托需要导入 setValue/getValue 导入 import androidx.compose.runtime.*
setContent 里直接写 UI 应该调用 Composable 函数 setContent { CounterScreen() }

7. Day 2 练习任务

  1. 完成上面的计数器应用,在模拟器中运行
  2. 添加第三个按钮"点我 -1",让 count 自减
  3. 加一个限制:count 不能小于 0(提示:if (count > 0) count--
  4. (挑战)把计数器的背景色随 count 变化:奇数用白色、偶数用浅灰色

四、Day 3:Dice Roller(骰子应用)--- 交互与随机

1. 为什么学这个

计数器只有一个数字变化。真实应用中,界面变化更丰富:图片切换、文字颜色变化、多控件协调更新。Dice Roller 是 Android 官方的经典入门项目,帮你巩固状态 + 按钮交互,同时引入随机数和图片资源。

2. 创建新项目

  1. New Project → Empty Activity
  2. Project Name:DiceRoller
  3. Language:Kotlin
  4. Minimum SDK:API 24

3. 准备骰子图片

app/src/main/res/ 下创建 drawable 目录,放入 6 张骰子图片,命名为:

text 复制代码
drawable/dice_1.png
drawable/dice_2.png
drawable/dice_3.png
drawable/dice_4.png
drawable/dice_5.png
drawable/dice_6.png

图片可以从 Android 官方 Codelab 素材 下载,或使用自己找的骰子图片。文件名只能用小写字母、数字和下划线。

4. 编写骰子界面

kotlin 复制代码
package com.example.diceroller

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DiceRollerApp()
        }
    }
}

@Composable
fun DiceRollerApp() {
    // 状态:当前骰子的点数(1~6)
    var diceValue by remember { mutableStateOf(1) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 根据 diceValue 显示对应的骰子图片
        val imageRes = when (diceValue) {
            1 -> R.drawable.dice_1
            2 -> R.drawable.dice_2
            3 -> R.drawable.dice_3
            4 -> R.drawable.dice_4
            5 -> R.drawable.dice_5
            else -> R.drawable.dice_6
        }

        Image(
            painter = painterResource(id = imageRes),
            contentDescription = "骰子点数 $diceValue",
            modifier = Modifier.size(200.dp)
        )

        Spacer(modifier = Modifier.height(32.dp))

        Button(onClick = {
            // 生成 1~6 的随机数
            diceValue = (1..6).random()
        }) {
            Text("掷骰子")
        }
    }
}

5. 代码逐段说明

状态的定义

kotlin 复制代码
var diceValue by remember { mutableStateOf(1) }
  • 初始值 1 表示刚打开时显示骰子 1
  • remember 保证旋转屏幕后不会重置为 1
  • mutableStateOf 保证值变化时 Image 自动切换

图片资源引用

kotlin 复制代码
val imageRes = when (diceValue) {
    1 -> R.drawable.dice_1
    2 -> R.drawable.dice_2
    ...
}
  • when 是 Kotlin 的分支语句(类似 Java 的 switch
  • 根据 diceValue 选择对应的资源 ID
  • R.drawable.dice_1:系统自动为 drawable/dice_1.png 生成的资源 ID

Image 组件

kotlin 复制代码
Image(
    painter = painterResource(id = imageRes),
    contentDescription = "骰子点数 $diceValue",
    modifier = Modifier.size(200.dp)
)
  • painterResource(id = ...):加载 res/drawable/ 中的图片
  • contentDescription:无障碍描述(和 View 的 contentDescription 一样重要)
  • Modifier.size(200.dp):设置宽高都是 200dp

随机数生成

kotlin 复制代码
diceValue = (1..6).random()
  • (1..6) 创建一个 1 到 6 的范围(IntRange)
  • .random() 从范围中随机取一个值
  • 这是 Kotlin 标准库函数,不需要额外导入

6. 运行效果

text 复制代码
┌──────────────────────┐
│                      │
│     ┌──────────┐     │
│     │          │     │
│     │    ⚃     │     │  ← 每次点击随机切换
│     │          │     │
│     └──────────┘     │
│                      │
│   ┌──────────┐      │
│   │  掷骰子   │      │
│   └──────────┘      │
│                      │
└──────────────────────┘

7. 常见错误

错误现象 原因 解决方法
图片不显示 文件名含大写字母或特殊字符 重命名为纯小写 + 数字 + 下划线
骰子总是 1,不变化 没有用 mutableStateOf 改成 by remember { mutableStateOf(...) }
旋转屏幕骰子重置为 1 忘记 remember 加上 remember
Unresolved reference: R 图片没放进 res/drawable/ 检查文件位置,Clean + Rebuild
painterResource 报错无法导入 忘记 import 导入 androidx.compose.ui.res.painterResource

8. Day 3 练习任务

  1. 完成 Dice Roller 应用,在模拟器中运行
  2. 修改骰子图片尺寸(例如改为 250dp),观察效果
  3. 在骰子图片上方加一个 Text,显示"当前点数:x"
  4. 添加一个"记录"功能:用 Text 显示上一次掷出的点数(提示:需要两个状态变量)

五、Day 4~5:小费计算器(Tip Calculator)--- 完整输入输出应用

1. 为什么学这个

计数器只有一个按钮、一个数字。骰子多了图片切换。小费计算器把文本输入、状态计算、格式化输出整合在一起,是 Unit 2 的综合实战项目。通过它可以完整体验 Compose 的"输入 → 状态 → UI"流程。

2. 创建新项目

  1. New Project → Empty Activity
  2. Project Name:TipCalculator
  3. Language:Kotlin
  4. Minimum SDK:API 24

3. 需求分析

小费计算器要完成以下功能:

功能 说明
输入消费金额 用户输入一个数字(如 100)
选择小费比例 提供几个选项:15%、18%、20%,默认 15%
自动计算 实时显示小费金额 = 消费金额 × 小费比例
金额格式化 结果显示为货币格式(如 ¥15.00)

4. 界面布局设计

text 复制代码
┌──────────────────────┐
│    小费计算器         │   ← 标题 Text
│                      │
│  ┌────────────────┐  │
│  │ 请输入消费金额   │  │   ← TextField(输入框)
│  └────────────────┘  │
│                      │
│  小费比例             │   ← 标签 Text
│  [ 15% ] [ 18% ] [ 20% ]  ← 三个 Button
│                      │
│  小费金额:¥15.00     │   ← 结果 Text
│                      │
└──────────────────────┘

5. 完整实现

kotlin 复制代码
package com.example.tipcalculator

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.NumberFormat
import java.util.Locale

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TipCalculatorScreen()
        }
    }
}

@Composable
fun TipCalculatorScreen() {
    // 状态 1:消费金额输入
    var amountInput by remember { mutableStateOf("") }

    // 状态 2:选中的小费比例
    var tipPercent by remember { mutableStateOf(15) }

    // 计算小费金额
    val amount = amountInput.toDoubleOrNull() ?: 0.0
    val tip = amount * tipPercent / 100.0

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 标题
        Text(
            text = "小费计算器",
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold
        )

        Spacer(modifier = Modifier.height(32.dp))

        // 输入框
        OutlinedTextField(
            value = amountInput,
            onValueChange = { newValue -> amountInput = newValue },
            label = { Text("请输入消费金额") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(24.dp))

        // 小费比例标签
        Text(
            text = "小费比例",
            fontSize = 18.sp
        )

        Spacer(modifier = Modifier.height(8.dp))

        // 三个比例按钮
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            val percentages = listOf(15, 18, 20)

            percentages.forEach { percent ->
                val isSelected = tipPercent == percent

                Button(
                    onClick = { tipPercent = percent },
                    colors = ButtonDefaults.buttonColors(
                        containerColor = if (isSelected) Color(0xFF6200EE) else Color(0xFFE0E0E0)
                    )
                ) {
                    Text(
                        text = "$percent%",
                        color = if (isSelected) Color.White else Color.Black
                    )
                }
            }
        }

        Spacer(modifier = Modifier.height(32.dp))

        // 计算结果
        val formatter = NumberFormat.getCurrencyInstance(Locale.CHINA)
        Text(
            text = "小费金额:${formatter.format(tip)}",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

6. 代码逐段说明

文本输入框(OutlinedTextField)

kotlin 复制代码
OutlinedTextField(
    value = amountInput,                          // 当前显示的文本
    onValueChange = { newValue ->                 // 用户每次输入时回调
        amountInput = newValue                    // 更新状态
    },
    label = { Text("请输入消费金额") },            // 占位标签
    modifier = Modifier.fillMaxWidth()
)
参数 说明
value 输入框显示的内容(必须绑定状态变量)
onValueChange 用户每次输入文字时调用,你在这里更新状态
label 输入框内顶部的提示标签

这是 Compose 中"双向绑定"的核心模式:状态 → 显示 → 用户输入 → 回调更新状态 → 重新显示。

关键:valueonValueChange 缺一不可。如果只写 value 不写 onValueChange,输入框会变成只读。

安全转换

kotlin 复制代码
val amount = amountInput.toDoubleOrNull() ?: 0.0
  • toDoubleOrNull():尝试把字符串转成 Double,转不了就返回 null(不会崩溃)
  • ?: 0.0:如果转换失败(空字符串或非数字),当作 0 处理
  • 这比 toDouble() 安全,不会因为输入了字母而闪退

小费计算

kotlin 复制代码
val tip = amount * tipPercent / 100.0
  • 消费金额 × (比例 / 100),例如 100 × 15/100 = 15

Row 布局 + 按钮高亮

kotlin 复制代码
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceEvenly   // 均匀分布
) {
    percentages.forEach { percent ->
        val isSelected = tipPercent == percent
        Button(
            onClick = { tipPercent = percent },
            colors = ButtonDefaults.buttonColors(
                containerColor = if (isSelected) 紫色 else 灰色  // 选中高亮
            )
        ) { ... }
    }
}
  • Row 相当于 LinearLayout horizontal,把子组件水平排列
  • Arrangement.SpaceEvenly:子组件等间距分布
  • isSelected 判断当前比例是否被选中,动态改按钮颜色

货币格式化

kotlin 复制代码
val formatter = NumberFormat.getCurrencyInstance(Locale.CHINA)
// 输出示例:¥15.00
  • NumberFormat.getCurrencyInstance() 根据地区自动格式化金额
  • Locale.CHINA 输出人民币符号 ¥,两位小数

7. 运行效果

text 复制代码
┌──────────────────────┐
│                      │
│     小费计算器        │
│                      │
│  ┌────────────────┐  │
│  │ 100            │  │  ← 输入 100
│  └────────────────┘  │
│                      │
│     小费比例          │
│  [ 15% ] [18%] [20%]  │  ← 15% 高亮
│                      │
│  小费金额:¥15.00     │
│                      │
└──────────────────────┘

输入 100,选择 18%,结果变为 ¥18.00------实时计算,不需要按"计算"按钮

8. 进阶优化:添加 Switch 让用户选择四舍五入方式

kotlin 复制代码
// 新增状态
var roundUp by remember { mutableStateOf(false) }

// 在界面中添加 Switch
// ...
// 修改计算逻辑
val tip = amount * tipPercent / 100.0
val finalTip = if (roundUp) kotlin.math.ceil(tip) else tip

说明:

  • kotlin.math.ceil() 向上取整,例如 14.3 → 15.0
  • Switch 是 Compose 的开关控件,用法和 OutlinedTextField 类似(value + onValueChange

9. 添加 Switch 的完整代码补充

在 Row 比例按钮下面添加:

kotlin 复制代码
import androidx.compose.material3.Switch

Spacer(modifier = Modifier.height(16.dp))

Row(
    verticalAlignment = Alignment.CenterVertically
) {
    Text("向上取整")
    Spacer(modifier = Modifier.width(8.dp))
    Switch(
        checked = roundUp,
        onCheckedChange = { roundUp = it }
    )
}

10. 常见错误

错误现象 原因 解决方法
输入框无法输入 onValueChange 没有更新状态 确保 onValueChange = { amountInput = it }
输入字母后闪退 用了 toDouble() 改用 toDoubleOrNull()
计算结果不更新 计算变量没有关联状态 确保计算依赖的值用 mutableStateOf 标记
输入框变成只读 忘记传 onValueChange 参数 OutlinedTextField 必须同时传 valueonValueChange
旋转屏幕后输入内容丢失 忘记 remember 所有状态变量都要包 remember

11. Day 4~5 练习任务

  1. 完成小费计算器应用,输入不同金额和比例测试
  2. 添加"向上取整"开关(参考进阶优化部分)
  3. 再加一个比例 25%(共 4 个按钮),调整 Row 布局使 4 个按钮排列美观
  4. 显示总金额(消费金额 + 小费),格式也为货币格式
  5. (挑战)当输入为空时,结果区域显示"请输入金额"而不是"¥0.00"

六、Unit 2 总结

知识清单

分类 掌握内容
Compose 基础 @ComposablesetContentTextColumnRowSpacerModifier
状态管理 remembermutableStateOfby 委托
按钮交互 Button(onClick = {})、点击事件中修改状态
文本输入 OutlinedTextFieldvalue + onValueChange 双向绑定
图片显示 Image + painterResourcecontentDescription
安全处理 toDoubleOrNull()?: 默认值
数学计算 kotlin.math.ceil()(1..6).random()
格式化 NumberFormat.getCurrencyInstance()"%.2f".format()
布局 Column(垂直)、Row(水平)、Modifier.fillMaxWidth()Arrangement

Compose vs Views 对比总结

概念 Views(第 3 周) Compose(本周)
布局文件 XML(activity_main.xml Kotlin 函数(@Composable
获取控件 findViewById / View Binding 不需要------状态变量本身就是数据源
按钮点击 setOnClickListener Button(onClick = {})
文本更新 textView.text = "xxx" 修改状态变量,Compose 自动重绘
输入框 EditText OutlinedTextField / TextField
垂直布局 LinearLayout vertical Column
水平布局 LinearLayout horizontal Row
占满宽度 layout_width="match_parent" Modifier.fillMaxWidth()
间距 layout_marginTop="16dp" Spacer(modifier = Modifier.height(16.dp))

自测清单

  • 能创建 Compose 项目(Empty Activity)
  • 能解释 @Composable 注解的作用
  • 能解释 remembermutableStateOf 的作用和区别
  • 能使用 Button(onClick = {}) 处理点击事件
  • 能使用 ColumnRow 排列控件
  • 能使用 OutlinedTextField 处理用户文本输入
  • 理解 value + onValueChange 的双向绑定模式
  • 能使用 toDoubleOrNull() 安全处理输入转换
  • 能使用 Image + painterResource 显示图片
  • 能独立完成计数器、骰子和 tip calculator 三个应用

七、综合练习:改进小费计算器

把本周学到的概念整合起来,改造你的小费计算器:

  1. 所有字符串用 res/values/strings.xml 管理(Compose 项目也支持)
  2. 输入验证:金额不能为负数(提示:amountInput.toDoubleOrNull()?.let { if (it < 0) ... }
  3. 添加"清空"按钮,点击后清空输入框并重置比例为 15%
  4. Column + Card 组件给结果区域加一个卡片样式
  5. 添加一个历史记录区域:显示最近 3 次计算的小费(提示:用 List 状态变量)

参考实现核心逻辑:

kotlin 复制代码
@Composable
fun TipCalculatorScreen() {
    var amountInput by remember { mutableStateOf("") }
    var tipPercent by remember { mutableStateOf(15) }
    var roundUp by remember { mutableStateOf(false) }
    var history by remember { mutableStateOf(listOf<String>()) }

    val amount = amountInput.toDoubleOrNull() ?: 0.0
    var tip = amount * tipPercent / 100.0
    if (roundUp) tip = kotlin.math.ceil(tip)
    val total = amount + tip

    val formatter = NumberFormat.getCurrencyInstance(Locale.CHINA)

    // ... 界面代码 ...

    // 清空按钮
    Button(onClick = {
        amountInput = ""
        tipPercent = 15
        roundUp = false
    }) {
        Text("清空")
    }
}

八、下一步学习

完成 Unit 2 后,你已经掌握了 Compose 的核心概念:声明式 UI、状态管理、用户交互。

下一步建议进入:

第 5 周 Unit 3:Compose 布局进阶(LazyColumn、ConstraintLayout Compose 版、Card)与 Compose 导航

如果觉得 Unit 2 的内容还不太扎实,可以先复习:

  • 把小费计算器重新做一遍,不看参考代码
  • 加一个"消费人数"功能,计算人均小费
  • 给骰子应用加一个"投掷次数统计"和"点数分布柱状图"(挑战题)

参考资料

相关推荐
菜鸟的日志3 小时前
【嵌入系统】嵌入式学习笔记(一)
windows·笔记·嵌入式硬件·学习·ubuntu·操作系统
深念Y3 小时前
装了 PowerShell 7 还是乱码?
windows·乱码·终端·命令行
相国4 小时前
在Windows里通过WSL安装Ubuntu 22.04
linux·windows·ubuntu·wsl
生信之灵4 小时前
追踪17只果蝇、7只线虫、10只小鼠,全程无需人工标注:这个无监督跟踪器如何颠覆动物行为研究?
人工智能·深度学习·神经网络·microsoft·交互
x***r1515 小时前
phpwind_UTF8_8.5部署步骤详解(附PHPWind论坛搭建与本地环境配置)
windows
酿情师6 小时前
网络攻防技术:Windows操作系统的攻防
网络·windows
倔强的石头1066 小时前
kingbase备份与恢复实战(六)—— 备份自动化与保留策略:Windows任务计划+日志追溯
运维·windows·自动化
FL16238631296 小时前
基于yolo26实现的免安装环境windows版一键训练工具
windows
YJlio7 小时前
8.2Windows 11 如何用 Xbox Game Bar 实时监测电脑性能?CPU、内存、GPU、显存与 FPS 瓶颈判断教程
windows·笔记·学习·chatgpt·架构·电脑·xbox