https://kotlinlang.org logo
#compose-android
Title
# compose-android
k

KotlinLeaner

03/08/2024, 11:25 PM
I have viewmodel on that base I want to navigate through screens. 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 am adding very basic example to explain my problem. MainActivity
Copy code
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            NavigationScreen()
        }
    }
}
MainViewModel
Copy code
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
Copy code
interface NavigationDestination {
    val route: String
}
NavigationHandler
Copy code
interface NavigationHandler {
    val destination: StateFlow<ScreenName?>
    fun navigate(navigationDestination: ScreenName)
}
Navigator
Copy code
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
Copy code
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
Copy 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")
        }
    }
}
s

Stylianos Gakis

03/09/2024, 9:52 AM
Looks like you're fighting the system by using NavHost and NavController along with saving another second source of truth regarding what destination you are in. I'd drop referencing your navigation stuff completely from your ViewModel and keep your NavController as the single source of truth regarding your destinations. If you then want to know which screen you're currently in, the nav controller exposes the current destination. If you absolutely want to do navigation from your ViewModels for some reason I'd probably look into other navigation solutions.
k

KotlinLeaner

03/09/2024, 12:02 PM
I'll change the viewmodel navigation from there. I am a little bit confused about the screen. The above code is very basic. I am fetching api call in real code so how can i know my composable to navigate which screen? Can you provide me with a basic example to fetch data and move screens accordingly?
k

KotlinLeaner

03/11/2024, 6:37 PM
Thanks for sharing it really helps
🙌 1