androidandroid-jetpack-composejetpack-compose-navigation

Correct flow for authorization with compose navigation


Which flow is required for correct behavior when the user opening an application with the ability to authenticate?

I am interested in two cases:

maybe there is some flow to check both cases (logged in locally, is the session up to date). While studying compose navigation, I came across the fact that I do not understand where I should do the check, pull the network request to check the session and redirect the user. It would be logical to do this on the SplashScreen (launch screen), but Android does not provide the ability to disable the native SplashScreen and use a custom one with logic since version 12

When answering please note that I am using jetpack compose and compose navigation


Solution

  • When you have an authentication-enabled app, you must gate your Compose Navigation graph behind a “splash” or “gatekeeper” route that performs both:

    1. Local state check (are we “logged in” locally?)

    2. Server/session check (is the user’s token still valid?)

    Because the Android 12+ native splash API is strictly for theming, you should:

    1. Define a SplashRoute as the first destination in your NavHost.

    2. In that composable, kick off your session‐validation logic (via a LaunchedEffect) and then navigate onward.


    1. Navigation Graph

    @Composable
    fun AppNavGraph(startDestination: String = Screen.Splash.route) {
      NavHost(navController = navController, startDestination = startDestination) {
        composable(Screen.Splash.route) { SplashRoute(navController) }
        composable(Screen.Login.route)  { LoginRoute(navController)  }
        composable(Screen.Home.route)   { HomeRoute(navController)   }
      }
    }
    
    

    2. SplashRoute Composable

    @Composable
    fun SplashRoute(
      navController: NavController,
      viewModel: SplashViewModel = hiltViewModel()
    ) {
      // Collect local-login flag and session status
      val sessionState by viewModel.sessionState.collectAsState()
    
      // Trigger a one‑time session check
      LaunchedEffect(Unit) {
        viewModel.checkSession()
      }
    
      // Simple UI while we wait
      Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator()
      }
    
      // React to the result as soon as it changes
      when (sessionState) {
        SessionState.Valid   -> navController.replace(Screen.Home.route)
        SessionState.Invalid -> navController.replace(Screen.Login.route)
        SessionState.Loading -> { /* still showing spinner */ }
      }
    }
    
    

    NavController extension

    To avoid back‑stack issues, you can define:

    fun NavController.replace(route: String) {
      navigate(route) {
        popUpTo(0) { inclusive = true }
      }
    }
    
    
    

    3. SplashViewModel

    @HiltViewModel
    class SplashViewModel @Inject constructor(
      private val sessionRepo: SessionRepository
    ) : ViewModel() {
    
      private val _sessionState = MutableStateFlow(SessionState.Loading)
      val sessionState: StateFlow<SessionState> = _sessionState
    
      /** Or call this from init { … } if you prefer. */
      fun checkSession() {
        viewModelScope.launch {
          // 1) Local check
          if (!sessionRepo.isLoggedInLocally()) {
            _sessionState.value = SessionState.Invalid
            return@launch
          }
    
          // 2) Remote/session check
          val ok = sessionRepo.verifyServerSession()
          _sessionState.value = if (ok) SessionState.Valid else SessionState.Invalid
        }
      }
    }
    
    

    4. SessionRepository Pseudocode

    class SessionRepository @Inject constructor(
      private val dataStore: UserDataStore,
      private val authApi: AuthApi
    ) {
      /** True if we have a non-null token cached locally. */
      suspend fun isLoggedInLocally(): Boolean =
        dataStore.currentAuthToken() != null
    
      /** Hits a “/me” or token‑refresh endpoint. */
      suspend fun verifyServerSession(): Boolean {
        return try {
          authApi.getCurrentUser().isSuccessful
        } catch (_: IOException) {
          false
        }
      }
    }
    
    

    Why this works

    Feel free to refine the API endpoints (e.g., refresh token on 401) or to prefetch user preferences after you land on Home, but this gatekeeper pattern is the industry standard.