如何让Jetpack Compose在不同布局分支复用同一可组合函数实例?
我根据不同屏幕尺寸设计了不同的可组合树结构,切换屏幕尺寸时会触发重组。但那些在树中位置变化的可组合函数,通过rememberSaveable、rememberLazyGridState等保存的状态值会被重复创建。
示例代码如下:
@Composable fun BasicPageLayout( firstColumnContent: @Composable ((modifier: Modifier, contentMaxWidth: Int) -> Unit)? = null, secondColumnButtonIcon: ImageVector? = null, secondColumnButtonText: String? = null, secondColumnTitle: String? = null, secondColumnContent: @Composable ((modifier: Modifier) -> Unit)? = null, ) { val windowWidthClass: WindowWidthSizeClass = LocalWindowWidthClass.current val contentMaxWidth: Int = if (secondColumnContent == null) 1500 else 780 when (windowWidthClass) { WindowWidthSizeClass.Expanded, // Expanded: Width > 840 WindowWidthSizeClass.Medium -> { // Medium: 600 < width < 840 Row(modifier = Modifier.fillMaxSize()) { firstColumnContent?.invoke(Modifier.weight(1f), contentMaxWidth) secondColumnContent?.invoke(Modifier.width(300.dp)) } } else -> { // Compact: Width < 600 Box(modifier = Modifier.fillMaxSize()) { firstColumnContent?.invoke(Modifier.fillMaxSize(), contentMaxWidth) secondColumnContent?.let { EndSideDrawer( secondColumnButtonIcon, secondColumnButtonText, secondColumnTitle, secondColumnContent, ) } } } } }
由于不同屏幕尺寸下firstColumnContent处于条件分支的不同位置,secondColumnContent还位于树的不同节点,Jetpack Compose会将它们视为不同的可组合实例,导致保存的状态重复。我希望让Compose在不同分支和节点复用firstColumnContent/secondColumnContent的同一实例,且不想将滚动位置这类状态提升至ViewModel,尝试给rememberSaveable传入静态key也无效。
要让Compose复用同一可组合实例,核心是让Compose能识别不同分支中的内容是同一个实体,以下是可行的实现方法:
1. 用key函数指定稳定标识
在调用firstColumnContent和secondColumnContent时,用key包裹并传入唯一且稳定的标识,让Compose明确这是同一个实例:
@Composable fun BasicPageLayout( firstColumnContent: @Composable ((modifier: Modifier, contentMaxWidth: Int) -> Unit)? = null, secondColumnButtonIcon: ImageVector? = null, secondColumnButtonText: String? = null, secondColumnTitle: String? = null, secondColumnContent: @Composable ((modifier: Modifier) -> Unit)? = null, ) { val windowWidthClass: WindowWidthSizeClass = LocalWindowWidthClass.current val contentMaxWidth: Int = if (secondColumnContent == null) 1500 else 780 when (windowWidthClass) { WindowWidthSizeClass.Expanded, WindowWidthSizeClass.Medium -> { Row(modifier = Modifier.fillMaxSize()) { firstColumnContent?.let { content -> key("first-column") { content(Modifier.weight(1f), contentMaxWidth) } } secondColumnContent?.let { content -> key("second-column") { content(Modifier.width(300.dp)) } } } } else -> { Box(modifier = Modifier.fillMaxSize()) { firstColumnContent?.let { content -> key("first-column") { content(Modifier.fillMaxSize(), contentMaxWidth) } } secondColumnContent?.let { content -> key("second-column") { EndSideDrawer( secondColumnButtonIcon, secondColumnButtonText, secondColumnTitle, content ) } } } } } }
这里的key值需稳定唯一,确保不同分支中同一内容的key一致,Compose就会复用之前的实例及其状态。
2. 提取内容到独立可组合函数并绑定key
把firstColumnContent和secondColumnContent的调用逻辑提取到独立可组合函数中,在函数内部指定key,让Compose识别复用:
@Composable private fun FirstColumnContainer( modifier: Modifier, contentMaxWidth: Int, content: @Composable ((modifier: Modifier, contentMaxWidth: Int) -> Unit) ) { key("first-column") { content(modifier, contentMaxWidth) } } @Composable private fun SecondColumnContainer( drawerParams: DrawerParams?, content: @Composable ((modifier: Modifier) -> Unit) ) { key("second-column") { if (drawerParams != null) { EndSideDrawer( drawerParams.icon, drawerParams.text, drawerParams.title, content ) } else { content(Modifier.width(300.dp)) } } } // 使用改造后的容器 @Composable fun BasicPageLayout(...) { val windowWidthClass: WindowWidthSizeClass = LocalWindowWidthClass.current val contentMaxWidth: Int = if (secondColumnContent == null) 1500 else 780 when (windowWidthClass) { WindowWidthSizeClass.Expanded, WindowWidthSizeClass.Medium -> { Row(modifier = Modifier.fillMaxSize()) { firstColumnContent?.let { FirstColumnContainer(Modifier.weight(1f), contentMaxWidth, it) } secondColumnContent?.let { SecondColumnContainer(null, it) } } } else -> { Box(modifier = Modifier.fillMaxSize()) { firstColumnContent?.let { FirstColumnContainer(Modifier.fillMaxSize(), contentMaxWidth, it) } secondColumnContent?.let { SecondColumnContainer( DrawerParams(secondColumnButtonIcon, secondColumnButtonText, secondColumnTitle), it ) } } } } } // 辅助数据类,用于传递抽屉参数 private data class DrawerParams( val icon: ImageVector?, val text: String?, val title: String? )
这种方式将内容的容器逻辑与标识逻辑分离,既保证代码清晰,又能稳定维持可组合实例的唯一性。
内容的提问来源于stack exchange,提问作者Alek Falkowski

