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

Jetpack Compose中LaunchedEffect内API调用未触发问题求助

问题根源

你的接口不是被LaunchedEffect漏触发,是应用在第一帧渲染时就提前崩溃,根本等不到协程发起API请求:

  1. 存储英雄信息的hero状态初始值是空列表listOf(),LaunchedEffect的协程会在Composable完成首次组合后才调度执行,但你在首次组合的执行流程中就直接写heroInfo[0]访问空列表的第0位元素,立刻触发数组越界崩溃,协程还没来得及启动网络请求,自然看不到API日志,应用直接闪退。
  2. ViewModel层协程写法存在冗余错误:getHeroInfo本身声明为suspend方法,内部却额外开启viewModelScope.launch新协程域,属于不规范的协程用法,反而容易导致调度时序异常。
修复步骤

1. 修正ViewModel层协程逻辑

去掉suspend方法内多余的协程嵌套,两种写法二选一即可,推荐直接用viewModelScope启动业务协程,符合ViewModel的职责设计:

@HiltViewModel
class HeroDetailViewModel @Inject constructor(
    private val repository: MarvelRepository
): ViewModel() {

    val loadError = mutableStateOf("")
    val isLoading = mutableStateOf(false)
    // 单条英雄详情没必要用列表存储,后续可以直接改类型为MutableState<MarvelListModel?> 初始值null,从根源避免数组越界
    val hero = mutableStateOf<List<MarvelListModel>>(emptyList())

    fun getHeroInfo(id : Long) {
        viewModelScope.launch {
            isLoading.value = true
            val resp = repository.getHeroInfo(id)
            when(resp){
                is WrapperResponse.Sucess -> {
                    hero.value = MarvelMapper().fromResponse(resp.data!!.dataResponse)
                    loadError.value = ""
                    isLoading.value = false
                }
                is WrapperResponse.Error -> {
                    loadError.value = resp.message!!
                    isLoading.value = false
                }
            }
        }
    }
}

2. 给Compose层增加状态兜底判断

在数据加载完成前,不要直接访问列表索引,根据加载中/加载失败/加载成功三种状态分别渲染对应UI:

@Composable
fun HeroDetailScreen(
    activity: Activity,
    context: Context,
    dominantColor: Color,
    heroNumber: Long,
    navController: NavController,
    topPadding: Dp = 20.dp,
    heroImageSize: Dp = 200.dp,
    viewModel: HeroDetailViewModel = hiltViewModel()
) {
    LaunchedEffect(key1 = heroNumber) {   
      viewModel.getHeroInfo(heroNumber)
    }
    val heroInfo = viewModel.hero.value
    val isLoading = viewModel.isLoading.value
    val loadError = viewModel.loadError.value

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(dominantColor)
            .padding(bottom = 16.dp)
    ) {
        // 加载中状态
        if (isLoading) {
            CircularProgressIndicator(
                modifier = Modifier.align(Alignment.Center),
                color = MaterialTheme.colors.primary
            )
        }
        // 加载错误状态
        else if (loadError.isNotEmpty()) {
            Text(
                text = loadError,
                color = Color.Red,
                modifier = Modifier.align(Alignment.Center)
            )
        }
        // 数据加载完成后再渲染详情内容
        else if (heroInfo.isNotEmpty()) {
            val currentHero = heroInfo[0]
            HeroDetailTopSection(
                activity = activity,
                navController = navController,
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight(0.2f)
                    .align(Alignment.TopCenter),
                context = context,
                viewModel = viewModel,
                heroInfo = currentHero,
                heroNumber = heroNumber.toInt()
            )
            HeroDetailStateWrapper(
                heroInfo = currentHero,
                modifier = Modifier
                    .fillMaxSize()
                    .padding(
                        top = topPadding + heroImageSize / 2f,
                        start = 16.dp,
                        end = 16.dp,
                        bottom = 16.dp
                    )
                    .shadow(10.dp, RoundedCornerShape(10.dp))
                    .clip(RoundedCornerShape(10.dp))
                    .background(MaterialTheme.colors.surface)
                    .padding(16.dp)
                    .align(Alignment.BottomCenter),
                loadingModifier = Modifier
                    .size(100.dp)
                    .align(Alignment.Center)
                    .padding(
                        top = topPadding + heroImageSize / 2f,
                        start = 16.dp,
                        end = 16.dp,
                        bottom = 16.dp
                    )
            )
            Box(
                contentAlignment = Alignment.TopCenter,
                modifier = Modifier.fillMaxSize()
            ) {
                currentHero.thumbnail.let {
                    CoilImage(
                        imageRequest = ImageRequest.Builder(LocalContext.current)
                            .data("$it")
                            .crossfade(true)
                            .build(),
                        imageLoader = {
                            ImageLoader.Builder(LocalContext.current)
                                .memoryCache(MemoryCache.Builder(LocalContext.current).maxSizePercent(0.25).build())
                                .crossfade(true)
                                .build()
                        },
                        contentDescription = currentHero.name,
                        modifier = Modifier
                            .size(heroImageSize)
                            .offset(y = topPadding)
                    )
                }
            }
        }
    }
}
额外优化建议
  • 单条英雄详情数据不要用List类型存储状态,直接定义为val hero = mutableStateOf<MarvelListModel?>(null),判断非空后再使用,从根源上避免数组越界问题。
  • 不要直接把Activity、Context作为参数透传给Composable,会破坏Compose预览能力、增加内存泄漏风险,需要上下文时用LocalContext.current获取,页面返回、支付触发这类事件通过回调参数传给上层处理。
  • 仓库层的异常捕获建议补充具体异常类型判断,不要统一返回未知错误,方便定位问题。

内容的提问来源于stack exchange,提问作者Manuel Lucas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 03:01:17