Android TV Compose:如何结合初始焦点恢复与popBackStack导航焦点恢复?
Android TV Compose 实现初始+导航返回的焦点恢复方案
问题根源
JetStream示例中的createInitialFocusRestorerModifiers()仅处理页面首次进入的焦点逻辑,未适配导航popBackStack()场景;同时TV Lazy列表会回收滚出视图的项,导致对应的FocusRequester实例被销毁,返回时直接调用已销毁的请求器就会触发「FocusRequester is not initialized」错误。
解决方案
核心思路是保存焦点项的唯一标识而非直接保存FocusRequester,在页面进入(含导航返回)时,通过标识定位目标项,确保项可见后再请求焦点。
1. 实现焦点状态管理器
用SavedStateHandle持久化焦点项的唯一key,同时管理FocusRequester的注册与注销:
class TvFocusRestorer( private val savedStateHandle: SavedStateHandle, private val focusKey: String = "tv_selected_item_key" ) { var selectedItemKey: String? by savedStateHandle.saveable { mutableStateOf(null) } private val focusRequesterMap = mutableMapOf<String, FocusRequester>() private val coroutineScope = CoroutineScope(Dispatchers.Main) fun requestFocusForKey(lazyListState: LazyListState) { selectedItemKey?.let { targetKey -> // 先检查目标项是否在可见区域 val visibleIndex = lazyListState.layoutInfo.visibleItemsIndices.firstOrNull { index -> lazyListState.layoutInfo.visibleItems[index].key == targetKey } if (visibleIndex != null) { coroutineScope.launch { delay(50) // 延迟确保项完成重组 focusRequesterMap[targetKey]?.requestFocus() } } else { // 目标项不在可见区,先滚动到对应位置 val targetIndex = lazyListState.layoutInfo.totalItemsCount.takeIf { it > 0 }?.let { total -> (0 until total).firstOrNull { index -> lazyListState.layoutInfo.items[index].key == targetKey } } ?: 0 coroutineScope.launch { lazyListState.scrollToItem(targetIndex) delay(50) focusRequesterMap[targetKey]?.requestFocus() ?: lazyListState.focusRequester.requestFocus() } } } ?: run { // 无保存状态,聚焦首个项 coroutineScope.launch { lazyListState.scrollToItem(0) delay(50) lazyListState.focusRequester.requestFocus() } } } fun registerFocusRequester(key: String, focusRequester: FocusRequester) { focusRequesterMap[key] = focusRequester } fun unregisterFocusRequester(key: String) { focusRequesterMap.remove(key) } }
2. 在Lazy列表中集成焦点恢复
给每个列表项绑定唯一key,管理焦点状态的保存与恢复:
@Composable fun TvContentRow( navController: NavHostController, savedStateHandle: SavedStateHandle, contentList: List<ContentItem> ) { val focusRestorer = remember { TvFocusRestorer(savedStateHandle) } val lazyListState = rememberLazyListState() // 页面初始化/返回时触发焦点恢复 LaunchedEffect(Unit) { focusRestorer.requestFocusForKey(lazyListState) } TvLazyRow( state = lazyListState, modifier = Modifier.focusRequester(lazyListState.focusRequester) ) { items( items = contentList, key = { item -> item.id } // 必须设置唯一key,确保项识别准确 ) { item -> val focusRequester = remember { FocusRequester() } // 注册/注销FocusRequester,避免内存泄漏 DisposableEffect(item.id) { focusRestorer.registerFocusRequester(item.id, focusRequester) onDispose { focusRestorer.unregisterFocusRequester(item.id) } } ContentItemCard( item = item, modifier = Modifier .focusRequester(focusRequester) .onFocusChanged { if (it.isFocused) { focusRestorer.selectedItemKey = item.id } }, onClick = { navController.navigate("detail/${item.id}") } ) } } }
关键注意事项
- 务必给每个Lazy项设置唯一key,保证列表重组时能精准定位目标项
- 用
DisposableEffect管理FocusRequester的生命周期,防止内存泄漏 - 滚动后添加短延迟再请求焦点,确保项已完成重组和请求器初始化
- 当目标项不在可见区域时,先滚动到对应位置再触发焦点请求
内容的提问来源于stack exchange,提问作者Firate
相关产品推荐
相关产品推荐

