Android实战项目③ Room+Clean Architecture开发待办事项App 完整源码详解

@[TOC](Android实战项目③ Room+Clean Architecture开发待办事项App 完整源码详解)# Android实战项目③ Room+Clean Architecture开发待办事项App 完整源码详解> 系列第3篇。学习Room数据库(Entity+DAO+Database)、DataStore偏好存储、Clean Architecture分层架构和Hilt接口绑定。---## 项目结构03-todo/app/src/main/java/├── com/example/todo/MainActivity.kt├── com/example/todo/MainApplication.kt├── com/example/todo/data/local/TodoDao.kt├── com/example/todo/data/local/TodoDatabase.kt├── com/example/todo/data/local/entity/TodoEntity.kt├── com/example/todo/data/repository/TodoRepositoryImpl.kt├── com/example/todo/di/DatabaseModule.kt├── com/example/todo/domain/model/Todo.kt├── com/example/todo/domain/model/TodoCategory.kt├── com/example/todo/domain/model/TodoPriority.kt├── com/example/todo/domain/repository/TodoRepository.kt├── com/example/todo/navigation/NavGraph.kt├── com/example/todo/ui/component/TodoItem.kt├── com/example/todo/ui/screen/AddEditTodoScreen.kt├── com/example/todo/ui/screen/TodoListScreen.kt├── com/example/todo/ui/theme/Theme.kt├── com/example/todo/viewmodel/AddEditTodoViewModel.kt├── com/example/todo/viewmodel/TodoListViewModel.kt文件数 : 18个Kotlin文件---## 完整源码 + 详解### MainActivity.ktkotlinpackage com.example.todoimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.navigation.compose.rememberNavControllerimport com.example.todo.navigation.TodoNavGraphimport com.example.todo.ui.theme.TodoThemeimport dagger.hilt.android.AndroidEntryPoint@AndroidEntryPointclass MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { TodoTheme { TodoNavGraph(rememberNavController()) } } }}---### MainApplication.ktkotlinpackage com.example.todoimport android.app.Applicationimport dagger.hilt.android.HiltAndroidApp@HiltAndroidAppclass MainApplication : Application()---### NavGraph.ktkotlinpackage com.example.todo.navigationimport androidx.compose.runtime.Composableimport androidx.navigation.NavHostControllerimport androidx.navigation.NavTypeimport androidx.navigation.compose.NavHostimport androidx.navigation.compose.composableimport androidx.navigation.navArgumentimport com.example.todo.ui.screen.AddEditTodoScreenimport com.example.todo.ui.screen.TodoListScreen@Composablefun TodoNavGraph(navController: NavHostController) { NavHost(navController, startDestination = "list") { composable("list") { TodoListScreen(onAddClick = { navController.navigate("add") }, onTodoClick = { id -> navController.navigate("edit/$id") }) } composable("add") { AddEditTodoScreen(onBack = { navController.popBackStack() }) } composable("edit/{todoId}", arguments = listOf(navArgument("todoId") { type = NavType.LongType })) { AddEditTodoScreen(onBack = { navController.popBackStack() }) } }}---### AddEditTodoViewModel.ktkotlinpackage com.example.todo.viewmodelimport androidx.lifecycle.SavedStateHandleimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.example.todo.domain.model.Todoimport com.example.todo.domain.model.TodoCategoryimport com.example.todo.domain.model.TodoPriorityimport com.example.todo.domain.repository.TodoRepositoryimport dagger.hilt.android.lifecycle.HiltViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport javax.inject.Inject@HiltViewModelclass AddEditTodoViewModel @Inject constructor( private val repository: TodoRepository, savedStateHandle: SavedStateHandle) : ViewModel() { private val todoId: Long = savedStateHandle.get<Long>("todoId") ?: -1L val title = MutableStateFlow("") val description = MutableStateFlow("") val category = MutableStateFlow(TodoCategory.OTHER) val priority = MutableStateFlow(TodoPriority.MEDIUM) private val _saved = MutableStateFlow(false) val saved = _saved.asStateFlow() init { if (todoId > 0) { viewModelScope.launch { repository.getTodoById(todoId)?.let { todo -> title.value = todo.title description.value = todo.description category.value = todo.category priority.value = todo.priority } } } } fun save() { if (title.value.isBlank()) return viewModelScope.launch { val todo = Todo( id = if (todoId > 0) todoId else 0, title = title.value, description = description.value, category = category.value, priority = priority.value ) if (todoId > 0) repository.updateTodo(todo) else repository.insertTodo(todo) _saved.value = true } }}---### TodoListViewModel.ktkotlinpackage com.example.todo.viewmodelimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport com.example.todo.domain.model.Todoimport com.example.todo.domain.model.TodoCategoryimport com.example.todo.domain.repository.TodoRepositoryimport dagger.hilt.android.lifecycle.HiltViewModelimport kotlinx.coroutines.ExperimentalCoroutinesApiimport kotlinx.coroutines.flow.*import kotlinx.coroutines.launchimport javax.inject.Inject@HiltViewModelclass TodoListViewModel @Inject constructor( private val repository: TodoRepository) : ViewModel() { private val _searchQuery = MutableStateFlow("") val searchQuery: StateFlow<String> = _searchQuery.asStateFlow() private val _selectedCategory = MutableStateFlow<TodoCategory?>(null) val selectedCategory: StateFlow<TodoCategory?> = _selectedCategory.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) val todos: StateFlow<List<Todo>> = _searchQuery.flatMapLatest { query -> if (query.isBlank()) repository.getAllTodos() else repository.searchTodos(query) }.combine(_selectedCategory) { todos, category -> if (category == null) todos else todos.filter { it.category == category } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) fun onSearchChanged(query: String) { _searchQuery.value = query } fun onCategorySelected(category: TodoCategory?) { _selectedCategory.value = category } fun toggleComplete(todo: Todo) { viewModelScope.launch { repository.toggleComplete(todo.id, !todo.isCompleted) } } fun deleteTodo(todo: Todo) { viewModelScope.launch { repository.deleteTodo(todo) } }}---### Todo.ktkotlinpackage com.example.todo.domain.model/** 待办领域模型 */data class Todo( val id: Long = 0, val title: String, val description: String = "", val category: TodoCategory = TodoCategory.OTHER, val priority: TodoPriority = TodoPriority.MEDIUM, val isCompleted: Boolean = false, val dueDate: Long? = null, val createdAt: Long = System.currentTimeMillis())---### TodoCategory.ktkotlinpackage com.example.todo.domain.model/** 待办分类 */enum class TodoCategory(val displayName: String) { WORK("工作"), PERSONAL("个人"), SHOPPING("购物"), STUDY("学习"), OTHER("其他")}---### TodoPriority.ktkotlinpackage com.example.todo.domain.model/** 优先级 */enum class TodoPriority(val displayName: String, val level: Int) { HIGH("高", 3), MEDIUM("中", 2), LOW("低", 1)}---### TodoRepository.ktkotlinpackage com.example.todo.domain.repositoryimport com.example.todo.domain.model.Todoimport kotlinx.coroutines.flow.Flowinterface TodoRepository { fun getAllTodos(): Flow<List<Todo>> fun searchTodos(query: String): Flow<List<Todo>> suspend fun getTodoById(id: Long): Todo? suspend fun insertTodo(todo: Todo): Long suspend fun updateTodo(todo: Todo) suspend fun deleteTodo(todo: Todo) suspend fun toggleComplete(id: Long, completed: Boolean)}---### TodoDao.ktkotlinpackage com.example.todo.data.localimport androidx.room.*import com.example.todo.data.local.entity.TodoEntityimport kotlinx.coroutines.flow.Flow/** DAO --- 数据访问对象 */@Daointerface TodoDao { @Query("SELECT * FROM todos ORDER BY isCompleted ASC, createdAt DESC") fun getAllTodos(): Flow<List<TodoEntity>> @Query("SELECT * FROM todos WHERE title LIKE '%' || :query || '%' OR description LIKE '%' || :query || '%'") fun searchTodos(query: String): Flow<List<TodoEntity>> @Query("SELECT * FROM todos WHERE id = :id") suspend fun getTodoById(id: Long): TodoEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTodo(todo: TodoEntity): Long @Update suspend fun updateTodo(todo: TodoEntity) @Delete suspend fun deleteTodo(todo: TodoEntity) @Query("UPDATE todos SET isCompleted = :completed WHERE id = :id") suspend fun toggleComplete(id: Long, completed: Boolean)}---### TodoDatabase.ktkotlinpackage com.example.todo.data.localimport androidx.room.Databaseimport androidx.room.RoomDatabaseimport com.example.todo.data.local.entity.TodoEntity@Database(entities = [TodoEntity::class], version = 1, exportSchema = false)abstract class TodoDatabase : RoomDatabase() { abstract fun todoDao(): TodoDao}---### TodoEntity.ktkotlinpackage com.example.todo.data.local.entityimport androidx.room.Entityimport androidx.room.PrimaryKeyimport com.example.todo.domain.model.Todoimport com.example.todo.domain.model.TodoCategoryimport com.example.todo.domain.model.TodoPriority/** Room 数据库实体 */@Entity(tableName = "todos")data class TodoEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val description: String = "", val category: String = "OTHER", val priority: String = "MEDIUM", val isCompleted: Boolean = false, val dueDate: Long? = null, val createdAt: Long = System.currentTimeMillis()) { fun toDomain() = Todo(id, title, description, TodoCategory.valueOf(category), TodoPriority.valueOf(priority), isCompleted, dueDate, createdAt) companion object { fun fromDomain(todo: Todo) = TodoEntity(todo.id, todo.title, todo.description, todo.category.name, todo.priority.name, todo.isCompleted, todo.dueDate, todo.createdAt) }}---### TodoRepositoryImpl.ktkotlinpackage com.example.todo.data.repositoryimport com.example.todo.data.local.TodoDaoimport com.example.todo.data.local.entity.TodoEntityimport com.example.todo.domain.model.Todoimport com.example.todo.domain.repository.TodoRepositoryimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.mapimport javax.inject.Injectimport javax.inject.Singleton@Singletonclass TodoRepositoryImpl @Inject constructor(private val dao: TodoDao) : TodoRepository { override fun getAllTodos(): Flow<List<Todo>> = dao.getAllTodos().map { list -> list.map { it.toDomain() } } override fun searchTodos(query: String): Flow<List<Todo>> = dao.searchTodos(query).map { list -> list.map { it.toDomain() } } override suspend fun getTodoById(id: Long): Todo? = dao.getTodoById(id)?.toDomain() override suspend fun insertTodo(todo: Todo): Long = dao.insertTodo(TodoEntity.fromDomain(todo)) override suspend fun updateTodo(todo: Todo) = dao.updateTodo(TodoEntity.fromDomain(todo)) override suspend fun deleteTodo(todo: Todo) = dao.deleteTodo(TodoEntity.fromDomain(todo)) override suspend fun toggleComplete(id: Long, completed: Boolean) = dao.toggleComplete(id, completed)}---### AddEditTodoScreen.ktkotlinpackage com.example.todo.ui.screenimport androidx.compose.foundation.layout.*import androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.automirrored.filled.ArrowBackimport androidx.compose.material.icons.filled.Checkimport androidx.compose.material3.*import androidx.compose.runtime.*import androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.hilt.navigation.compose.hiltViewModelimport com.example.todo.domain.model.TodoCategoryimport com.example.todo.domain.model.TodoPriorityimport com.example.todo.viewmodel.AddEditTodoViewModel@OptIn(ExperimentalMaterial3Api::class)@Composablefun AddEditTodoScreen(viewModel: AddEditTodoViewModel = hiltViewModel(), onBack: () -> Unit) { val title by viewModel.title.collectAsState() val description by viewModel.description.collectAsState() val category by viewModel.category.collectAsState() val priority by viewModel.priority.collectAsState() val saved by viewModel.saved.collectAsState() LaunchedEffect(saved) { if (saved) onBack() } Scaffold(topBar = { TopAppBar(title = { Text("编辑待办") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "返回") } }, actions = { IconButton(onClick = { viewModel.save() }) { Icon(Icons.Default.Check, "保存") } }) }) { padding -> Column(Modifier.fillMaxSize().padding(padding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { OutlinedTextField(value = title, onValueChange = { viewModel.title.value = it }, label = { Text("标题") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = description, onValueChange = { viewModel.description.value = it }, label = { Text("描述") }, modifier = Modifier.fillMaxWidth(), minLines = 3) // 分类选择 Text("分类", style = MaterialTheme.typography.labelLarge) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { TodoCategory.entries.forEach { cat -> FilterChip(selected = category == cat, onClick = { viewModel.category.value = cat }, label = { Text(cat.displayName) }) } } // 优先级 Text("优先级", style = MaterialTheme.typography.labelLarge) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { TodoPriority.entries.forEach { p -> FilterChip(selected = priority == p, onClick = { viewModel.priority.value = p }, label = { Text(p.displayName) }) } } } }}---### TodoListScreen.ktkotlinpackage com.example.todo.ui.screenimport androidx.compose.foundation.layout.*import androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.LazyRowimport androidx.compose.foundation.lazy.itemsimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Addimport androidx.compose.material3.*import androidx.compose.runtime.*import androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.hilt.navigation.compose.hiltViewModelimport com.example.todo.domain.model.TodoCategoryimport com.example.todo.ui.component.TodoItemimport com.example.todo.viewmodel.TodoListViewModel@OptIn(ExperimentalMaterial3Api::class)@Composablefun TodoListScreen( viewModel: TodoListViewModel = hiltViewModel(), onAddClick: () -> Unit, onTodoClick: (Long) -> Unit) { val todos by viewModel.todos.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState() val selectedCategory by viewModel.selectedCategory.collectAsState() Scaffold( topBar = { TopAppBar(title = { Text("待办事项") }) }, floatingActionButton = { FloatingActionButton(onClick = onAddClick) { Icon(Icons.Default.Add, "新增") } } ) { padding -> Column(Modifier.fillMaxSize().padding(padding)) { // 搜索 OutlinedTextField(value = searchQuery, onValueChange = { viewModel.onSearchChanged(it) }, placeholder = { Text("搜索...") }, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), singleLine = true) // 分类过滤 LazyRow(contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { item { FilterChip(selected = selectedCategory == null, onClick = { viewModel.onCategorySelected(null) }, label = { Text("全部") }) } items(TodoCategory.entries.toList()) { cat -> FilterChip(selected = selectedCategory == cat, onClick = { viewModel.onCategorySelected(cat) }, label = { Text(cat.displayName) }) } } // 列表 LazyColumn(contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { items(todos, key = { it.id }) { todo -> TodoItem(todo = todo, onToggle = { viewModel.toggleComplete(todo) }, onDelete = { viewModel.deleteTodo(todo) }, onClick = { onTodoClick(todo.id) }) } } } }}---### Theme.ktkotlinpackage com.example.todo.ui.themeimport android.os.Buildimport androidx.compose.foundation.isSystemInDarkThemeimport androidx.compose.material3.*import androidx.compose.runtime.Composableimport androidx.compose.ui.platform.LocalContext@Composablefun TodoTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colorScheme = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val ctx = LocalContext.current if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx) } darkTheme -> darkColorScheme() else -> lightColorScheme() } MaterialTheme(colorScheme = colorScheme, content = content)}---### TodoItem.ktkotlinpackage com.example.todo.ui.componentimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.*import androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Deleteimport androidx.compose.material3.*import androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.style.TextDecorationimport androidx.compose.ui.text.style.TextOverflowimport androidx.compose.ui.unit.dpimport com.example.todo.domain.model.Todo@Composablefun TodoItem(todo: Todo, onToggle: () -> Unit, onDelete: () -> Unit, onClick: () -> Unit) { Card(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick)) { Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = todo.isCompleted, onCheckedChange = { onToggle() }) Column(modifier = Modifier.weight(1f).padding(start = 8.dp)) { Text( todo.title, style = MaterialTheme.typography.titleSmall, textDecoration = if (todo.isCompleted) TextDecoration.LineThrough else null, maxLines = 1, overflow = TextOverflow.Ellipsis ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { AssistChip(onClick = {}, label = { Text(todo.category.displayName) }) AssistChip(onClick = {}, label = { Text(todo.priority.displayName) }) } } IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, "删除") } } }}---### DatabaseModule.ktkotlinpackage com.example.todo.diimport android.content.Contextimport androidx.room.Roomimport com.example.todo.data.local.TodoDaoimport com.example.todo.data.local.TodoDatabaseimport com.example.todo.data.repository.TodoRepositoryImplimport com.example.todo.domain.repository.TodoRepositoryimport dagger.Bindsimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.android.qualifiers.ApplicationContextimport dagger.hilt.components.SingletonComponentimport javax.inject.Singleton@Module@InstallIn(SingletonComponent::class)object DatabaseModule { @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): TodoDatabase = Room.databaseBuilder(context, TodoDatabase::class.java, "todo.db").build() @Provides fun provideTodoDao(db: TodoDatabase): TodoDao = db.todoDao()}@Module@InstallIn(SingletonComponent::class)abstract class RepositoryModule { @Binds @Singleton abstract fun bindTodoRepository(impl: TodoRepositoryImpl): TodoRepository}---## 运行方式1. Android Studio打开 projects/03-todo 目录2. Gradle同步完成后Run运行> 系列导航: Android实战系列 1-6 篇。

相关推荐
恋猫de小郭4 小时前
2026 Android I/O ,全新 AI 手机、 Android PC 和车载驾驶
android·前端·flutter
赏金术士17 小时前
Kotlin ViewModel
android·kotlin
vistaup18 小时前
kotlin 二维码实现高斯模糊
android·kotlin
愈努力俞幸运19 小时前
function calling与mcp
android·数据库·redis
阿巴斯甜20 小时前
LeakCanary
android
阿巴斯甜20 小时前
compose
android
阿巴斯甜20 小时前
Glide
android
-SOLO-20 小时前
使用Perfetto debug trace查看超时slice
android
阿巴斯甜20 小时前
Retrofit
android
阿巴斯甜20 小时前
OkHttp
android