You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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))
        }
    }
}
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 04:21:50