LaunchedEffect抛出“协程作用域已离开组合”异常的排查与解决
问题:TvLazyRow内请求焦点抛出
LeftCompositionCancellationException UI结构代码
父级TvLazyColumn
TvLazyColumn(){ itemsIndexed(rows){index, row -> MyLazyRow(mainViewModel, row, index) } }
MyLazyRow.kt
@Composable fun MyLazyRow(mainViewModel : MainViewModel, row : List<rowItem>, listIndex : Int){ val myLazyState = rememberTvLazyListState() val myRequesters by remember(row.size) { derivedStateOf { Array(row.size) { FocusRequester() } } } TvLazyRow(state = myLazyState){ itemsIndexed(row){ index, rowItem -> MyItem(item = rowItem, modifier = Modifier.focusRequester(myRequesters[index])) } } LaunchedEffect(Unit){ if (listIndex == mainViewModel.getLastFocussedRow.value){ try { myLazyState.scrollToItem(dummyTargetFocus) // 自定义的目标索引常量 myRequesters[dummyTargetFocus].requestFocus() } catch (ex: Exception) { ex.printStackTrace() } } } }
异常信息
androidx.compose.runtime.LeftCompositionCancellationException: The coroutine scope left the composition
即便确认目标LazyRow和子项已完成组合,仍触发该异常,以下是原因分析和解决办法:
异常原因
- LaunchedEffect生命周期不匹配:
LaunchedEffect(Unit)仅在组件首次组合时执行一次,若mainViewModel.getLastFocussedRow.value的变化滞后于MyLazyRow的组合完成,协程执行时当前MyLazyRow可能已因TvLazyColumn滚动被移出组合,导致协程所在的CompositionScope失效。 - FocusRequester状态不稳定:用
derivedStateOf创建的myRequesters数组,会在row.size变化时重新生成,若协程执行时数组已被替换,旧的FocusRequester已与当前组件解绑,请求焦点时触发组合取消异常。 - scrollToItem的异步特性:
scrollToItem是异步操作,直接后续调用requestFocus时,目标Item可能尚未完成重组,FocusRequester未关联到实际可组合项,进而触发异常。
可行解决办法
1. 调整LaunchedEffect触发条件,绑定关联状态
将LaunchedEffect的触发key改为listIndex和mainViewModel.getLastFocussedRow.value的组合,确保状态变化时重新触发协程,且协程生命周期与组件组合状态绑定:
LaunchedEffect(listIndex, mainViewModel.getLastFocussedRow.value) { if (listIndex == mainViewModel.getLastFocussedRow.value) { // 先校验目标索引的有效性 if (dummyTargetFocus in row.indices) { myLazyState.scrollToItem(dummyTargetFocus) awaitFrame() // 等待下一帧确保Item完成重组 myRequesters[dummyTargetFocus].requestFocus() } } }
2. 稳定FocusRequester的创建方式
移除derivedStateOf,改用remember直接持有数组,确保row.size不变时数组实例不会重建:
// 替换原有的myRequesters定义 val myRequesters = remember(row.size) { Array(row.size) { FocusRequester() } }
3. 用DisposableEffect管理协程生命周期
通过DisposableEffect配合rememberCoroutineScope,在组件离开组合时自动取消协程,避免无效操作:
val coroutineScope = rememberCoroutineScope() DisposableEffect(listIndex, mainViewModel.getLastFocussedRow.value) { val job = coroutineScope.launch { if (listIndex == mainViewModel.getLastFocussedRow.value && dummyTargetFocus in row.indices) { myLazyState.scrollToItem(dummyTargetFocus) awaitFrame() myRequesters[dummyTargetFocus].requestFocus() } } // 组件离开组合时取消协程 onDispose { job.cancel() } }
4. 等待滚动完成后再请求焦点
利用TvLazyListState的isScrollInProgress状态,等待滚动操作结束后再执行焦点请求:
LaunchedEffect(listIndex, mainViewModel.getLastFocussedRow.value) { if (listIndex == mainViewModel.getLastFocussedRow.value && dummyTargetFocus in row.indices) { myLazyState.scrollToItem(dummyTargetFocus) // 等待滚动操作结束 while (myLazyState.isScrollInProgress) { delay(16) } awaitFrame() myRequesters[dummyTargetFocus].requestFocus() } }
内容的提问来源于stack exchange,提问作者Rahul Rawat
相关产品推荐
相关产品推荐

