79689946

Date: 2025-07-04 09:30:48
Score: 0.5
Natty:
Report link

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.

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • Starts with a question (0.5): When you have an
  • Low reputation (1):
Posted by: Ajeet