Jetpack Compose中LaunchedEffect内API调用未触发问题求助
问题根源
你的接口不是被LaunchedEffect漏触发,是应用在第一帧渲染时就提前崩溃,根本等不到协程发起API请求:
- 存储英雄信息的
hero状态初始值是空列表listOf(),LaunchedEffect的协程会在Composable完成首次组合后才调度执行,但你在首次组合的执行流程中就直接写heroInfo[0]访问空列表的第0位元素,立刻触发数组越界崩溃,协程还没来得及启动网络请求,自然看不到API日志,应用直接闪退。 - 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
相关产品推荐
相关产品推荐

