Search code examples
androidkotlinandroid-jetpack-compose

Jetpack Compose navigation library problem - conditional navigation


Basically, the problem I'm dealing with is that I have two screens: OnBoarding and login. When the user installs and opens the app for the first time I save true to indicate that it was the first time.

When the user opens the app more than once, it's intended to send them to the login screen, but first shows the onboarding screen for a few seconds and then navigates to the login screen, see the code below:

@Composable
fun Navigation() {
    val navController = rememberNavController()
    val onBoardingViewModel = hiltViewModel<OnBoardingViewModel>()
    val isTheFirstTimeInstalled = onBoardingViewModel.onBoardingState.collectAsStateWithLifecycle()
    val startDest = getStartDest(isTheFirstTimeInstalled.value)
    NavHost(
        navController = navController,
        startDestination = startDest,
        enterTransition = { slideIn() },
        exitTransition = { slideOut() },
    ) { /* some code */ }
}

OnBoardingViewModel:

@HiltViewModel
class OnBoardingViewModel @Inject constructor(
    private val dataStore: PreferencesDatastore
) : ViewModel() {
    private val _onBoardingState = MutableStateFlow(false)
    val onBoardingState = _onBoardingState.asStateFlow()

    init {
        viewModelScope.launch(Dispatchers.IO) {
            _onBoardingState.value = dataStore.getOnBoardingState().first()
        }
    }

    fun setFirstTimeInstalled() {
        viewModelScope.launch(Dispatchers.IO) {
            dataStore.saveOnBoardingState(true)
        }
    }
}

Is there any way to solve this problem? Let me know please!

I only tried to determine the start destination by calling my function "getStartDest" which basically gets the route


Solution

  • The problem is that you have actually three cases:

    1. Opened for the first time
    2. Not opened for the first time
    3. Undetermined

    The third one occurs when the data store hasn't provided a value yet. You can use a Splashscreen to pass the time so neither the OnBoarding nor the Login screen is displayed. It could even be as simple as just displaying a CircularProgressIndicator().

    Now, for this to work your view model should already incorporate these three cases. The easiest way to do it would be this:

    val onBoardingState: StateFlow<Boolean?> = dataStore.getOnBoardingState()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = null,
        )
    

    The MutableStateFlow and the init block aren't needed anymore, instead the data store flow is directly returned as a StateFlow with initialValue = null. As you can see, the type is now a Boolean? that is nullable. This allows you to differentiate the third case, 3. Undetermined: The StateFlow immediately has a value (null), even when the underlying flow from the data store does not.

    As a side note: Please don't collect flows in the view model (as you did with the first() function). Flows should only be collected where they are needed, that is the UI. Everything in between should only be transformations, as shown above.

    Now you just need to update getStartDest to also handle the case null (Splashscreen, CircularProgressIndicator, whatever) and you're set.