为何ViewModel状态变化时未依赖该状态的路由Composable也会重组?
问题根因
你观察到的路由层Composable意外重组,核心原因非常清晰:
你在路由的composable(Screen.Home.route) { ... }作用域内,直接读取了ViewModel内部的State状态值homeViewModel.homeCount,哪怕你只是把这个值作为参数传给子Composable,只要在当前作用域完成了State的读取操作,当前Composable就会自动订阅这个State的变更通知,后续State更新时自然会触发路由层的重组。
这是Compose重组机制的固有逻辑,很多开发者会存在误解:Compose的重组订阅粒度是「Composable作用域 + 该作用域内读取的State」,和你读取State之后用来做什么没有关系,只要完成了读取动作就会建立订阅关系。
修复方案
这里提供两种通用的修复方案,可根据实际业务场景选择:
方案1:将ViewModel实例下沉到页面对应Composable内部获取
直接把hiltViewModel()的调用从路由层移到HomeScreen内部,路由层完全不涉及ViewModel操作,自然不会读取State触发重组:
// 路由层修改 private fun NavGraphBuilder.addHome() { composable(Screen.Home.route) { Log.i("Home", "Route Composable") // 直接调用HomeScreen,无需传递ViewModel相关参数 HomeScreen() } } // HomeScreen修改 @Composable fun HomeScreen() { val homeViewModel = hiltViewModel<HomeViewModel>() Log.i("Home", "Home Screen") Column(modifier = Modifier.fillMaxSize()) { Spacer(modifier = Modifier.height(25.dp)) Button(onClick = { homeViewModel.updateCount() }) { Text("Increment count") } Spacer(modifier = Modifier.height(25.dp)) Text("Count is: ${homeViewModel.homeCount}") } }
方案2:路由层传参时用Lambda包装State读取操作
如果出于业务需要必须在路由层初始化ViewModel,可以将页面的参数修改为取值Lambda,避免在路由作用域直接读取State:
// 路由层修改 private fun NavGraphBuilder.addHome() { composable(Screen.Home.route) { Log.i("Home", "Route Composable") val homeViewModel = hiltViewModel<HomeViewModel>() HomeScreen( // 传递属性引用/Lambda,不实际读取State值 homeCount = { homeViewModel.homeCount }, updateCount = { homeViewModel.updateCount() }, ) } } // HomeScreen修改 @Composable fun HomeScreen( homeCount: () -> Int, updateCount: () -> Unit, ) { Log.i("Home", "Home Screen") Column(modifier = Modifier.fillMaxSize()) { Spacer(modifier = Modifier.height(25.dp)) Button(onClick = updateCount) { Text("Increment count") } Spacer(modifier = Modifier.height(25.dp)) // 仅在HomeScreen作用域内读取State,订阅变更 Text("Count is: ${homeCount()}") } }
两种方案都可以保证只有实际使用State的HomeScreen会在状态更新时触发重组,路由层不会被意外调用。
内容的提问来源于stack exchange,提问作者Jesse Hill
相关产品推荐
相关产品推荐

