How do I pass argument from NavHost to ViewModel?

80 Views Asked by At

Could you please help me? How can I pass the argument from NavHost and get it in ViewModel? I found a solution using SavedStateHandle but it doesn't work.

@HiltViewModel

class NewsScreenViewModel @Inject constructor(
    val savedStateHandle: SavedStateHandle,
) : ViewModel() {

  private val _sourceStatus = MutableStateFlow(savedStateHandle.get("source") ?: null)
    val getSourceStatus: StateFlow<String> get() = _sourceStatus

    private val _internetStatus = MutableStateFlow(ConnectivityObserver.Status.UNAVAILABLE)
    val internetStatus: StateFlow<ConnectivityObserver.Status> get() = _internetStatus

  private val _sourceStatus = MutableStateFlow(savedStateHandle.get("source") ?: FeedSource.NEXTWEB.sourceString )
    val getSourceStatus: StateFlow<String> get() = _sourceStatus

NavHost looks like this. During the debug process I founnd out that there is a value coming to source variable but not retrieved in ViewModel.

NavHost(navController = navController, startDestination = Screens.NewsScreen.route) {

    composable(
        route = "news/{source}",
        arguments = listOf(
            navArgument("source") {
                type = NavType.StringType
                defaultValue = ""
            }
        )
    ) { backStackEntry ->
       val source = backStackEntry.arguments?.getString("source") ?: "nullsource"
        NewsScreen(viewModel = newsScreenViewModel, navController = navController, paddingValues = paddingValues)
    }

This is a section for bottom navigation. When I navigate from one tab to another it should pass an argument.

@Composable
fun BottomNavigationSection(navController: NavHostController, items: List<Screens>) {
    BottomNavigation {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { screen ->
            BottomNavigationItem(
                modifier = Modifier.background(colorResource(id = R.color.grey))
                    .fillMaxSize() ,
                icon = {
                    Icon(
                        painterResource(id = screen.icon),
                        contentDescription = screen.route,
                    )
                },
                selected = currentRoute == screen.route,
                onClick = {
                    if (currentRoute != screen.route) {
                        navController.navigate(screen.route)
                    }
                }
            )
        }
    }
}
2

There are 2 best solutions below

4
Jan Itor On

Your NewsScreen should get its view model from hiltViewModel() function:

@Composable
fun NewsScreen(
    viewModel: NewsScreenViewModel = hiltViewModel(),
    //...
)

And not injected manually:

//...
NewsScreen(/*viewModel = newsScreenViewModel,*/ navController = navController, paddingValues = paddingValues)
//...
0
tasjapr On

You can do this by passing argument to the composable function parameter

In the NavHost:

    composable(route = "news/{source}") { backStackEntry ->
       val source = backStackEntry.arguments?.getString("source") ?: "nullsource"
        NewsScreen(
            viewModel = newsScreenViewModel, 
            source = source, // pass argument to the screen function
            navController = navController, 
            paddingValues = paddingValues
        )
    }

Add source to your composable screen and handle changes:

@Composable
fun NewsScreen(..., source: String) {
        LaunchedEffect(key1 = source) {
              viewModel.setSource(source)
        }
}

In the ViewModel add setSource(source: String) function and save the source value:

    private val _source = MutableStateFlow("")

    fun setSource(source: String) {
        // This check is necessary because when you press the "back" button, 
        // the value of the argument passed to the screen will be equal to zero
        if (_source.value != source) _source.value = id 
    }

BTW, it's better to pass only lambda functions to your nested composables, so you don't need to pass the NavHostController to the BottomNavigationSection function, only the onClick lambda is needed.

It will be good for composable previews and for code readability. You can move logic with navigation to the NavHost and pass navigation lambdas to the screen.