LESSON S4

实战:待办清单 App

从数据类到状态,再到按钮交互

概念 · 示例 · 练习 · 小测
预计 15 分钟Android Studio

先用人话理解

这份小应用用数据类表达待办,用 remember 保存输入和列表状态,用 LazyColumn 显示任务。你可以添加、完成/撤销和删除任务。为了把重点放在 Compose 状态上,数据目前只在运行期间保存在内存中,关闭应用后不会保留。

本课要点:此示例的列表状态只保存在内存中,关闭应用后不会持久化。正式应用可进一步学习 ViewModel、Room 与状态提升。

代码例子

先读注释,再关注每一行如何把数据变成结果。

ANDROID · MainActivity.kt
// MainActivity.kt:使用 Android Studio 的 Empty Activity (Compose) 模板
// 若项目包名不同,请把第一行 package 改成项目实际包名。
package com.example.todo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

data class Todo(val id: Int, val title: String, val done: Boolean = false)

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

@Composable
fun TodoScreen() {
    var nextId by remember { mutableIntStateOf(3) }
    var input by remember { mutableStateOf("") }
    var todos by remember { mutableStateOf(listOf(Todo(1, "学习 Kotlin"), Todo(2, "练习 Compose"))) }
    Column(Modifier.fillMaxSize().padding(20.dp)) {
        Text("待办清单", style = MaterialTheme.typography.headlineMedium)
        OutlinedTextField(
            value = input,
            onValueChange = { input = it },
            label = { Text("写下一个待办") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
        Button(
            onClick = {
                val title = input.trim()
                if (title.isNotEmpty()) {
                    todos = todos + Todo(nextId++, title)
                    input = ""
                }
            },
            enabled = input.isNotBlank()
        ) { Text("添加任务") }
        LazyColumn { items(todos, key = { it.id }) { todo ->
            Row(Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween) {
                Text(if (todo.done) "✓ ${todo.title}" else todo.title, modifier = Modifier.weight(1f))
                TextButton(onClick = { todos = todos.map { if (it.id == todo.id) it.copy(done = !it.done) else it } }) { Text(if (todo.done) "撤销" else "完成") }
                TextButton(onClick = { todos = todos.filterNot { it.id == todo.id } }) { Text("删除") }
            }
        } }
    }
}

再看一个用法

COMPOSE · 状态更新片段
// 状态更新示例:只修改被点击的那一条,其他待办保持不变
fun toggleTodo(todos: List<Todo>, selectedId: Int): List<Todo> =
    todos.map { item ->
        if (item.id == selectedId) item.copy(done = !item.done) else item
    }

动手改一改

代码阅读路线: 1. Todo 是数据模型,id 用于稳定识别每一项。 2. remember 保存当前界面状态;添加任务时创建新列表。 3. LazyColumn 只按需组合可见项目。 4. map + copy 只更新被点击的任务,其他任务保持不变。 练习:输入“喝水”并添加;再试试完成、撤销和删除这项任务,观察列表状态如何变化。

查看 Android / Compose 环境指南 ↗

提示:Android 专属示例需要 Android Studio 项目和对应依赖;普通 Kotlin 示例可在 Playground 中尝试。

运行要求:请在 Android Studio 中创建或打开启用了 Jetpack Compose 的 Empty Activity 项目。把示例 package 行改成项目实际包名;Compose 依赖由项目模板配置。此代码不能直接粘贴进普通 Kotlin Playground。

三道小测

先自己选择,再展开答案。答错也没关系,重要的是知道为什么。

1. Todo 数据类主要表达什么?

查看答案与解析

正确答案:A。回到概念和代码示例,再解释一次为什么。

2. 为什么列表项使用稳定的 id?

查看答案与解析

正确答案:A。回到概念和代码示例,再解释一次为什么。

3. remember 保存的数据会自动写入数据库吗?

查看答案与解析

正确答案:B。抓住题目中的核心概念,重新对照示例理解。

深入阅读

想继续往下挖,可以阅读 Kotlin / Android 官方文档。

打开官方文档 ↗

标记学习进度

进度保存在当前浏览器的 localStorage 中。