Jetpack Compose Todo应用协程报错求助:数据获取失败
解决Jetpack Compose Todo应用协程数据获取异常问题
问题根源分析
- 重复启动协程:
HomeScreen每次Compose重组都会调用todoViewModel.showTasks(),导致多次启动协程,旧协程被取消后触发DiagnosticCoroutineContextException。 - 状态未适配Compose响应式:若
dataState不是MutableStateFlow这类可观察状态容器,Compose无法感知状态变化,直接赋值还可能引发线程安全问题。 - 流收集逻辑错误:数据库返回的
Flow是持续推送更新的流,collectLatest会一直挂起,导致finally块提前将loading设为false,不符合加载状态逻辑。 - 空值不安全操作:界面中大量使用
!!强制非空,一旦数据为空会触发NPE,进而引发协程异常。 - Toast调用不规范:在Composable重组流程中直接调用
Toast.makeText,会导致重复弹出Toast,违反Compose副作用处理规则。
修复步骤
1. 修正ViewModel状态与协程逻辑
将dataState改为MutableStateFlow,确保Compose能响应状态变化,同时调整流收集逻辑:
class TodoViewModel(private val todoRepository: TodoRepository) : ViewModel() { // 使用MutableStateFlow作为响应式状态容器 private val _dataState = MutableStateFlow(DataState()) val dataState = _dataState.asStateFlow() // 初始化时启动一次数据收集,避免重复触发 init { showTasks() } fun showTasks() = viewModelScope.launch { try { _dataState.update { it.copy(loading = true, error = null) } // Room返回的Flow会持续推送数据变化,直接collect即可 todoRepository.getTasks().collect { tasks -> _dataState.update { when { tasks.isEmpty() -> it.copy(data = emptyList(), error = "List is empty ...", loading = false) else -> it.copy(data = tasks, error = null, loading = false) } } } } catch (ex: Exception) { _dataState.update { it.copy(error = ex.localizedMessage, loading = false) } } } } // 定义标准化的状态数据类 data class DataState( val loading: Boolean = false, val data: List<Task> = emptyList(), val error: String? = null )
2. 修正HomeScreen界面逻辑
- 使用
collectAsState观察ViewModel状态 - 移除重复调用
showTasks()的代码 - 处理空值安全,避免强制非空
- 用
LaunchedEffect处理Toast副作用
@Composable fun HomeScreen(navController: NavController, todoViewModel: TodoViewModel = hiltViewModel() ) { val state by todoViewModel.dataState.collectAsState() val context = LocalContext.current // 仅在error变化时弹出Toast,避免重复触发 state.error?.let { errorMsg -> LaunchedEffect(errorMsg) { Toast.makeText(context, errorMsg, Toast.LENGTH_LONG).show() } } Column(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxWidth()) { Text( "Reminder", modifier = Modifier .fillMaxWidth() .weight(1f) .padding(top = 10.dp, start = 20.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold ) IconButton(onClick = { navController.navigate(ScreenNavigation.Add.route) }) { Icon(imageVector = Icons.Filled.Add, contentDescription = "Add task", tint = Color.Black) } } // 加载状态优先展示 if (state.loading) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } else { when { state.data.isEmpty() && state.error == null -> { // 空数据提示 Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("No tasks yet", fontSize = 18.sp) } } else -> { LazyColumn( modifier = Modifier.padding(top = 10.dp, start = 20.dp, end = 20.dp, bottom = 40.dp) ) { items(state.data) { task -> Row( modifier = Modifier .fillMaxWidth() .height(60.dp) .padding(vertical = 8.dp) ) { Text( text = task.task ?: "Unnamed task", modifier = Modifier.weight(1f), fontSize = 19.sp, fontWeight = FontWeight.Bold ) Text( text = task.time ?: "No time set", fontSize = 17.sp, fontWeight = FontWeight.W500 ) } } } } } } } }
3. 额外注意事项
- 确保
todoRepository.getTasks()返回Flow<List<Task>>(Room数据库默认支持此类型,实现数据实时同步) - 若仅需一次性获取数据而非实时监听,可替换为
todoRepository.getTasks().first(),此时finally块可正确关闭加载状态 - 禁止在Composable重组流程中直接调用ViewModel业务方法,应放在ViewModel初始化或用户交互事件(如按钮点击)中
内容的提问来源于stack exchange,提问作者Taki
相关产品推荐
相关产品推荐

