/ Jetpack Compose  Android  UI开发  Kotlin  MVVM  状态管理  Room数据库  Material3 

Android Jetpack Compose 实战:从零构建一个完整的 ToDo 应用


文章封面

Jetpack Compose 已经从“尝鲜”阶段进入了主流 Android 开发的视野。越来越多的团队开始在新项目中使用 Compose,但很多开发者在真正上手时才发现:看懂文档是一回事,写出能跑的完整应用又是另一回事。

本文用一个真实的 ToDo 应用作为载体,从项目搭建到最终运行,完整走一遍 Compose 开发流程,包括状态管理、数据持久化、MVVM 架构、UI 交互等核心知识点,并附上踩坑记录。

一、项目结构与依赖配置

先看整体项目结构,我们按照标准 MVVM 分层:

app/
├── data/
│   ├── Task.kt
│   ├── TaskDao.kt
│   └── TaskDatabase.kt
├── repository/
│   └── TaskRepository.kt
├── viewmodel/
│   └── TaskViewModel.kt
└── ui/
    ├── MainActivity.kt
    ├── TaskListScreen.kt
    └── AddTaskScreen.kt

在 build.gradle.kts 中添加依赖:

android {
    compileSdk = 34
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
    buildFeatures { compose = true }
}
dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
    implementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    ksp("androidx.room:room-compiler:$roomVersion")
    implementation("androidx.navigation:navigation-compose:2.7.6")
}

踩坑提示:Compose 编译器版本必须与 Kotlin 版本严格对应。

二、数据层:Room 数据库实现

@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val title: String,
    val description: String = "",
    val isCompleted: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)

@Dao
interface TaskDao {
    @Query("SELECT * FROM tasks ORDER BY createdAt DESC")
    fun getAllTasks(): Flow>
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: Task)
    @Update
    suspend fun updateTask(task: Task)
    @Delete
    suspend fun deleteTask(task: Task)
}

三、Repository 与 ViewModel

class TaskRepository(private val taskDao: TaskDao) {
    val allTasks: Flow> = taskDao.getAllTasks()
    suspend fun insertTask(task: Task) = taskDao.insertTask(task)
    suspend fun updateTask(task: Task) = taskDao.updateTask(task)
    suspend fun deleteTask(task: Task) = taskDao.deleteTask(task)
}

class TaskViewModel(application: Application) : AndroidViewModel(application) {
    private val repository: TaskRepository
    val allTasks: StateFlow>
    var newTaskTitle by mutableStateOf("")
        private set
    init {
        val db = TaskDatabase.getDatabase(application)
        repository = TaskRepository(db.taskDao())
        allTasks = repository.allTasks
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
    }
    fun addTask() {
        if (newTaskTitle.isBlank()) return
        viewModelScope.launch {
            repository.insertTask(Task(title = newTaskTitle.trim()))
            newTaskTitle = ""
        }
    }
    fun toggleTaskCompletion(task: Task) {
        viewModelScope.launch { repository.updateTask(task.copy(isCompleted = !task.isCompleted)) }
    }
    fun deleteTask(task: Task) {
        viewModelScope.launch { repository.deleteTask(task) }
    }
}

踩坑提示:在 Compose 里推荐用 StateFlow + collectAsStateWithLifecycle(),能感知生命周期,避免在后台仍然收集数据浪费资源。

四、UI 层:任务列表页面

@Composable
fun TaskListScreen(viewModel: TaskViewModel = viewModel(), onNavigateToAdd: () -> Unit) {
    val tasks by viewModel.allTasks.collectAsStateWithLifecycle()
    Scaffold(
        topBar = { TopAppBar(title = { Text("我的待办") }) },
        floatingActionButton = {
            FloatingActionButton(onClick = onNavigateToAdd) {
                Icon(Icons.Default.Add, contentDescription = "添加任务")
            }
        }
    ) { paddingValues ->
        if (tasks.isEmpty()) {
            Box(modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center) {
                Text("暂无任务,点击右下角添加")
            }
        } else {
            LazyColumn(modifier = Modifier.padding(paddingValues), contentPadding = PaddingValues(16.dp)) {
                items(tasks, key = { it.id }) { task ->
                    TaskItem(task = task, onToggle = { viewModel.toggleTaskCompletion(task) }, onDelete = { viewModel.deleteTask(task) })
                }
            }
        }
    }
}

@Composable
fun TaskItem(task: Task, onToggle: () -> Unit, onDelete: () -> Unit) {
    Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
        Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Checkbox(checked = task.isCompleted, onCheckedChange = { onToggle() })
            Text(task.title, modifier = Modifier.weight(1f),
                textDecoration = if (task.isCompleted) TextDecoration.LineThrough else TextDecoration.None)
            IconButton(onClick = onDelete) {
                Icon(Icons.Default.Delete, contentDescription = "删除")
            }
        }
    }
}

五、添加任务页面与导航配置

@Composable
fun AddTaskScreen(viewModel: TaskViewModel = viewModel(), onTaskAdded: () -> Unit) {
    Scaffold(topBar = { TopAppBar(title = { Text("添加任务") }) }) { paddingValues ->
        Column(modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp)) {
            OutlinedTextField(
                value = viewModel.newTaskTitle,
                onValueChange = { viewModel.newTaskTitle = it },
                label = { Text("任务名称") },
                modifier = Modifier.fillMaxWidth()
            )
            Button(onClick = { viewModel.addTask(); onTaskAdded() },
                modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
                enabled = viewModel.newTaskTitle.isNotBlank()) {
                Text("保存任务")
            }
        }
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TodoAppTheme {
                val navController = rememberNavController()
                NavHost(navController, startDestination = "list") {
                    composable("list") { TaskListScreen(onNavigateToAdd = { navController.navigate("add") }) }
                    composable("add") { AddTaskScreen(onTaskAdded = { navController.popBackStack() }) }
                }
            }
        }
    }
}

六、常见踩坑总结

  • remember vs rememberSaveable:remember 在重组时保持状态,横竖屏切换后会丢失;需要跨配置变更保留状态用 rememberSaveable。

  • LazyColumn key 参数:一定要给 items 传 key 参数,否则列表更新时动画不正确。

  • Modifier 顺序很重要:.padding().clickable() 和 .clickable().padding() 效果完全不同。

  • AndroidViewModel 与 ViewModel:需要访问 Context 时用 AndroidViewModel,否则普通 ViewModel 即可。

七、扩展方向

  • 优先级与分类:给 Task 增加 priority 和 category 字段,UI 上用颜色区分

  • 截止日期提醒:集成 WorkManager 设置本地通知

  • Widget 支持:使用 Glance API 在桌面展示待办列表

  • 深色模式:Material3 自动支持动态主题,只需在 Theme.kt 中开启 dynamicColor = true

如果在实际项目中遇到其他 Compose 相关问题,欢迎留言交流!

发布评论

热门评论区: