I have viewmodel on that base I want to navigate through screens. I am adding very basic example to explain my problem.
MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationScreen()
}
}
}
MainViewModel
class MainViewModel(private val navigationHandler: NavigationHandler) : ViewModel() {
val currentDestination: StateFlow<ScreenName?> =
navigationHandler.destination.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
initialValue = null
)
fun navigateToDeviceSelection(isValid: Boolean) {
val destination = if (isValid) {
ScreenName.ScreenOne
} else {
ScreenName.ScreenTwo
}
println(">> destination ${destination.route}")
navigationHandler.navigate(destination)
}
}
I created a custom navigation class which handle the logic and navigate the screen accordingly using StateFlow.
NavigationDestination
interface NavigationDestination {
val route: String
}
NavigationHandler
interface NavigationHandler {
val destination: StateFlow<ScreenName?>
fun navigate(navigationDestination: ScreenName)
}
Navigator
class Navigator : NavigationHandler {
private val _destination: MutableStateFlow<ScreenName?> = MutableStateFlow(null)
override val destination: StateFlow<ScreenName?> = _destination.asStateFlow()
override fun navigate(navigationDestination: ScreenName) {
_destination.value = navigationDestination
}
}
ScreenName
sealed class ScreenName(override val route: String) : NavigationDestination {
object ScreenOne : ScreenName("ScreenOne") {
object ChildOne : ScreenName("ChildOne")
object ChildTwo : ScreenName("ChildTwo")
}
object ScreenTwo : ScreenName("ScreenTwo")
}
Now when I consume this MainViewModel inside composable like below code
@Composable
fun NavigationScreen(
viewModel: MainViewModel = koinViewModel(),
navController: NavHostController = rememberNavController()
) {
val destinationState by viewModel.currentDestination.collectAsStateWithLifecycle()
LaunchedEffect(destinationState) {
destinationState?.let {
navController.navigate(it.route) {
popUpTo(navController.graph.startDestinationId) {
inclusive = true
}
launchSingleTop = true
}
}
}
LaunchedEffect(viewModel) {
viewModel.navigateToDeviceSelection(true)
}
NavHost(
navController = navController,
startDestination = ScreenName.ScreenOne.route,
route = "parentRoute"
) {
nestedGraphSample(navController, viewModel)
composable(ScreenName.ScreenTwo.route) {
Text(text = "Screen Two Route")
}
}
}
private fun NavGraphBuilder.nestedGraphSample(
navController: NavHostController,
viewModel: MainViewModel
) {
navigation(
startDestination = ScreenName.ScreenOne.ChildOne.route,
route = ScreenName.ScreenOne.route
) {
composable(ScreenName.ScreenOne.ChildOne.route) {
LaunchedEffect(Unit) {
delay(5.seconds)
navController.navigate(ScreenName.ScreenOne.ChildTwo.route) {
popUpTo(navController.graph.startDestinationId) {
inclusive = true
}
launchSingleTop = true
}
}
Text(text = "Screen Child One Route")
}
composable(ScreenName.ScreenOne.ChildTwo.route) {
LaunchedEffect(Unit) {
delay(5.seconds)
viewModel.navigateToDeviceSelection(true)
}
Text(text = "Screen Child Two Route")
}
}
}
My primary issue arises when the user initially invokes navigateToDeviceSelection, leading to redirection to ScreenName.ScreenOne and the storage of this value within destinationState. Subsequently, when navigating via navController, the destination state remains unchanged, resulting in redirection to another screen. Additionally, upon invoking navigateToDeviceSelection within ScreenName.ScreenOne.ChildTwo.route, the destination state fails to update, rendering the LaunchedEffect ineffective, and causing the user to remain stuck in the ScreenName.ScreenOne.ChildTwo.route screen. To address this challenge, I seek a solution that avoids accessing destinationState within nested composable functions, as this example is basic, and I prefer not to pass this variable to nested children.
I think to update your code to the Android developer guidance, the shortest path is:
stateInon a StateFlow, creating a redundant StateFlow. I would just delete the entire NavigationHandler and Navigator, but if you don't, you need to at least remove the redundant StateFlow and need to pass through this new function.But like I said, I think you can simplify your code quite a bit by eliminating the NavigationHandler and Navigator.
You might also consider clarifying some of your function names. For example
NavigationHandler.navigate()andMainViewModel.navigateToDestination()do not actually perform any navigation. They only set the desired new destination. It makes it confusing that your Composable has to tell the ViewModel to navigate and then it also has to tell the NavHost to navigate, but these are doing two different things.