Jetpack Compose:如何用SavedStateHandle在Hilt ViewModel间传参?
问题描述
我之前在应用中使用Navigation组件做简单的页面参数传递,引入Hilt ViewModel后,了解到SavedStateHandle可以实现便捷的页面间传参。我已经在HiltViewModel中实现了相关代码,但现在需要把muscleGroup参数从HomeScreen传递到SampleExercisesScreen,同时要让这个参数能被ExerciseViewModel以及其他页面使用。
现有代码
ExerciseViewModel代码
@HiltViewModel class ExerciseViewModel @Inject constructor( private val repository: ExerciseRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { val data: MutableState<DataOrException<List<Exercise>, Boolean, Exception>> = mutableStateOf( DataOrException(null, true, Exception("")) ) val muscleGroup: String? = savedStateHandle[Constants.MUSCLE_GROUP_KEY] private val _exerciseListFromDb = MutableStateFlow<List<Exercise>>(emptyList()) val exerciseListFromDb = _exerciseListFromDb.asStateFlow() init { getExercises() viewModelScope.launch(Dispatchers.IO) { repository.getAllExercisesFromDb().collect() { _exerciseListFromDb.value = it } } } private fun getExercises() { viewModelScope.launch { data.value.loading = true data.value = repository.getExercises() if (data.value.data.toString().isNotEmpty()) data.value.loading = false } } fun insertExerciseToDb(exercise: Exercise) = viewModelScope.launch { repository.insertExerciseToDb(exercise) } fun deleteExerciseFromDb(exercise: Exercise) = viewModelScope.launch { repository.deleteExerciseFromDb(exercise) } }
HomeScreen代码
@Composable fun HomeScreen(navController: NavController) { Surface(modifier = Modifier.fillMaxSize(), color = AppColors.mBackground) { Column { Header() Row(modifier = Modifier .fillMaxWidth() .padding(top = 50.dp)){ MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.body, muscleGroup = "Chest", navController) MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.male, muscleGroup = "Back", navController) MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.shoulder, muscleGroup = "Shoulders", navController) } Row(modifier = Modifier.fillMaxWidth()){ MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.muscle, muscleGroup = "Biceps", navController) MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.triceps, muscleGroup = "Triceps", navController) MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.leg, muscleGroup = "Legs", navController) } } } }
SampleExerciseScreen代码
@Composable fun SampleExerciseScreen(navController: NavController, muscleGroup: String, exerciseList: List<Exercise>?) { val mExerciseList = exerciseList!!.filter { it.muscle == muscleGroup } Log.d("TEST", "$mExerciseList, $muscleGroup") Surface(modifier = Modifier.fillMaxSize(), color = AppColors.mBackground) { Column { MyTopBar(navController = navController) LazyColumn(Modifier.weight(1f)){ items(mExerciseList) { ExerciseRow(exercise = it) } } GoToButton(navController = navController, text = "YOUR EXERCISES", route = Screen.UserExercises.passMuscleGroup(muscleGroup)) } } }
NavGraph代码
@Composable fun SetupNavGraph(navController: NavHostController, viewModel: ExerciseViewModel) { val exerciseList = viewModel.data.value.data?.toList() val exerciseListFromDb = viewModel.exerciseListFromDb.collectAsState().value val muscleGroup = viewModel.muscleGroup NavHost(navController = navController, startDestination = Screen.Home.route) { composable( route = Screen.Home.route ) { HomeScreen(navController = navController) } composable( route = Screen.SampleExercise.route, ) { SampleExerciseScreen( navController = navController, muscleGroup = muscleGroup.toString(), exerciseList = exerciseList ) } composable( route = Screen.UserExercises.route, arguments = listOf(navArgument(MUSCLE_GROUP_KEY) { type = NavType.StringType }) ) { UserExercisesScreen( navController = navController, muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(), viewModel = viewModel, exerciseListFromDb = exerciseListFromDb ) } composable( route = Screen.Add.route, arguments = listOf(navArgument(MUSCLE_GROUP_KEY) { type = NavType.StringType }) ) { AddScreen( navController = navController, muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(), viewModel = viewModel ) } } }
解决方案步骤
1. 修正Screen类中SampleExercise的路由定义
给Screen.SampleExercise的路由添加参数占位符,让Navigation能识别并传递参数:
enum class Screen(val route: String) { Home("home"), // 添加参数占位符,格式为{参数名} SampleExercise("sample_exercise/{${Constants.MUSCLE_GROUP_KEY}}"), UserExercises("user_exercises/{${Constants.MUSCLE_GROUP_KEY}}"), Add("add/{${Constants.MUSCLE_GROUP_KEY}}"); // 生成带参数的路由字符串 fun passMuscleGroup(muscleGroup: String): String { return route.replace("{${Constants.MUSCLE_GROUP_KEY}}", muscleGroup) } }
2. 更新NavGraph中SampleExercise的配置
在NavGraph里给SampleExercise的composable添加参数声明,这样Navigation会自动把参数存入ViewModel的SavedStateHandle:
composable( route = Screen.SampleExercise.route, // 声明参数类型 arguments = listOf(navArgument(Constants.MUSCLE_GROUP_KEY) { type = NavType.StringType }) ) { backStackEntry -> // 直接从路由栈拿参数,或用ViewModel中已获取的muscleGroup val muscleGroup = backStackEntry.arguments?.getString(Constants.MUSCLE_GROUP_KEY) ?: "" SampleExerciseScreen( navController = navController, muscleGroup = muscleGroup, exerciseList = viewModel.data.value.data?.toList() ) }
3. 修改HomeScreen中MuscleButton的导航逻辑
确保点击按钮时传递muscleGroup参数到SampleExerciseScreen:
@Composable fun MuscleButton( modifier: Modifier, icon: Int, muscleGroup: String, navController: NavController ) { Button( modifier = modifier, onClick = { // 使用Screen类的方法生成带参数的路由 navController.navigate(Screen.SampleExercise.passMuscleGroup(muscleGroup)) } ) { Image(painter = painterResource(id = icon), contentDescription = muscleGroup) Text(text = muscleGroup) } }
4. 优化ViewModel中muscleGroup的响应式(可选)
如果需要监听muscleGroup的变化,可以把它改成StateFlow:
// 在ExerciseViewModel中修改 val muscleGroup: StateFlow<String?> = savedStateHandle.getStateFlow(Constants.MUSCLE_GROUP_KEY, null)
在Composable中可以这样使用:
val muscleGroup by viewModel.muscleGroup.collectAsState()
5. 保持其他页面的参数逻辑一致
UserExercises和Add页面已经正确配置了参数传递,ViewModel会自动从SavedStateHandle获取参数,无需额外修改。
内容的提问来源于stack exchange,提问作者deja
相关产品推荐
相关产品推荐

