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

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和子项已完成组合,仍触发该异常,以下是原因分析和解决办法:


异常原因

  1. LaunchedEffect生命周期不匹配:LaunchedEffect(Unit)仅在组件首次组合时执行一次,若mainViewModel.getLastFocussedRow.value的变化滞后于MyLazyRow的组合完成,协程执行时当前MyLazyRow可能已因TvLazyColumn滚动被移出组合,导致协程所在的CompositionScope失效。
  2. FocusRequester状态不稳定:用derivedStateOf创建的myRequesters数组,会在row.size变化时重新生成,若协程执行时数组已被替换,旧的FocusRequester已与当前组件解绑,请求焦点时触发组合取消异常。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 04:02:36