You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose Todo应用协程报错求助:数据获取失败

解决Jetpack Compose Todo应用协程数据获取异常问题

问题根源分析

  1. 重复启动协程:HomeScreen每次Compose重组都会调用todoViewModel.showTasks(),导致多次启动协程,旧协程被取消后触发DiagnosticCoroutineContextException。
  2. 状态未适配Compose响应式:若dataState不是MutableStateFlow这类可观察状态容器,Compose无法感知状态变化,直接赋值还可能引发线程安全问题。
  3. 流收集逻辑错误:数据库返回的Flow是持续推送更新的流,collectLatest会一直挂起,导致finally块提前将loading设为false,不符合加载状态逻辑。
  4. 空值不安全操作:界面中大量使用!!强制非空,一旦数据为空会触发NPE,进而引发协程异常。
  5. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 21:05:20