Jetpack Compose导航图作用域ViewModel如何优雅获取路由参数
composable(routeWithParameter) { // (A): 此处需要从路由中获取'args'参数 // 我当前的解决方案是重新添加该参数: val args = it.arguments?.getString("argumentKey") // 从路由中获取'args' val state = it.getRememberedParent(navController) // 获取父导航栈条目 state.arguments?.putString("argumentKey", args ) // 将'args'重新存入父状态的参数中 val viewModel = hiltViewModel<TViewModel>(state) Log.d("MISC", "id =${System.identityHashCode(viewModel)}") // 与(B)中的实例相同 ... } composable(route) { // (B): val viewModel = hiltViewModel<TViewModel>(it.getRememberedParent(navController)) Log.d("MISC", "id =${System.identityHashCode(viewModel)}") //与(A)中的实例相同 ... } ...
}
@Composable
fun NavBackStackEntry.getRememberedParent(navController: NavHostController): NavBackStackEntry {
val parentId = this.destination.parent!!.id
return remember(this) { navController.getBackStackEntry(parentId) }
}
ViewModel通过`SavedStateHandle`获取参数: ```kotlin @HiltViewModel class EditFarmViewModel @Inject constructor( state: SavedStateHandle, ) : ViewModel() { private val _args = checkNotNull(state.get<String>("argumentKey")) }
为了让(A)和(B)拿到同一个ViewModel实例,我不能在(A)中直接调用不带state参数的hiltViewModel(),否则(B)会创建新的实例。我在官方文档里没找到对应的解决技巧。
我有两个疑问:
- 如何在不重新插入参数的前提下,让ViewModel获取到(A)的路由参数?
- 或者,如何在(A)中不用
getBackStackEntry查找父条目,就能拿到同一个作用域的ViewModel实例,同时还能使用初始路由参数?
方案一:将参数提升到父导航图路由(推荐)
最符合导航组件设计规范的方式,是把参数直接绑定到父导航图的路由上。这样当你导航到父导航图下的任何页面时,参数会自动存在于父导航栈条目的arguments中,ViewModel可以直接从父作用域的SavedStateHandle读取,完全不需要手动复制参数。
示例代码:
// 定义包含参数的父导航图 navigation( startDestination = "route_with_param", route = "parent_route/{argumentKey}" ) { composable("route_with_param") { backStackEntry -> // 直接获取父导航栈条目 val parentBackStackEntry = backStackEntry.getRememberedParent(navController) // 从父作用域获取ViewModel,参数已自动存在于SavedStateHandle中 val viewModel = hiltViewModel<EditFarmViewModel>(parentBackStackEntry) // 后续使用viewModel } composable("route_without_param") { backStackEntry -> val parentBackStackEntry = backStackEntry.getRememberedParent(navController) val viewModel = hiltViewModel<EditFarmViewModel>(parentBackStackEntry) // 后续使用viewModel } }
导航到父导航图时,需要传入参数:
navController.navigate("parent_route/your_argument_value")
方案二:直接给ViewModel的SavedStateHandle设置参数
如果不想修改导航图结构,可以在(A)页面中获取到参数后,将其设置到父作用域ViewModel的SavedStateHandle中,只需确保只设置一次(避免页面重建时重复设置):
首先修改ViewModel,暴露SavedStateHandle:
@HiltViewModel class EditFarmViewModel @Inject constructor( val savedStateHandle: SavedStateHandle, // 直接暴露SavedStateHandle ) : ViewModel() { private val _args: String get() = checkNotNull(savedStateHandle.get<String>("argumentKey")) }
然后在(A)页面中:
composable(routeWithParameter) { backStackEntry -> val args = checkNotNull(backStackEntry.arguments?.getString("argumentKey")) val parentBackStackEntry = backStackEntry.getRememberedParent(navController) val viewModel = hiltViewModel<EditFarmViewModel>(parentBackStackEntry) // 仅在参数变化时设置一次 LaunchedEffect(args) { viewModel.savedStateHandle.set("argumentKey", args) } // 后续使用viewModel }
方案三:结合Hilt辅助注入传递参数
如果ViewModel有其他依赖,同时需要手动传递参数,可以使用Hilt的AssistedInject来实现,既保留依赖注入能力,又能传递路由参数:
- 修改ViewModel,添加
AssistedInject和辅助工厂:
@HiltViewModel class EditFarmViewModel @AssistedInject constructor( @Assisted state: SavedStateHandle, private val farmRepository: FarmRepository // 其他依赖 ) : ViewModel() { private val _args = checkNotNull(state.get<String>("argumentKey")) // 定义辅助工厂 @AssistedFactory interface Factory { fun create(state: SavedStateHandle): EditFarmViewModel } // 提供ViewModel工厂 companion object { fun provideFactory( factory: Factory, argumentKey: String ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return factory.create(SavedStateHandle(mapOf("argumentKey" to argumentKey))) as T } } } }
- 创建Hilt入口点:
@EntryPoint @InstallIn(ActivityComponent::class) interface ViewModelFactoryEntryPoint { fun editFarmViewModelFactory(): EditFarmViewModel.Factory }
- 在(A)页面中使用:
composable(routeWithParameter) { backStackEntry -> val args = checkNotNull(backStackEntry.arguments?.getString("argumentKey")) val parentBackStackEntry = backStackEntry.getRememberedParent(navController) // 获取Hilt工厂 val factory = EntryPointAccessors.fromActivity( LocalContext.current as Activity, ViewModelFactoryEntryPoint::class.java ).editFarmViewModelFactory() // 从父作用域获取ViewModel,传入参数 val viewModel = viewModel<EditFarmViewModel>( viewModelStore = parentBackStackEntry.viewModelStore, factory = EditFarmViewModel.provideFactory(factory, args) ) // 后续使用viewModel }
内容的提问来源于stack exchange,提问作者pierre.b

