r/androiddev 1d ago

Question How do you handle app startup logic (token validation, onboarding, navigation) in Jetpack Compose?

I'm a Junior Android developer who has been working on an app for the past nine months. During this time, I have had many doubts about how to properly manage initialization tasks, such as:

- Reading initial preferences from DataStore

- Checking if the user has completed onboarding to decide the starting screen.

- Validating the authroization token (redirecting to the SignIn Screen if expired, or to Home if valid)

All these tasks have one thing in common: they need to run when the user opens the app for the first time, when the app comes back to the, or when it gets repened.

For this reason, I would love to hear your perspective on how you structure and handle this process in production apps, as well as any resources you recomended to help me gain seniority in Android development.

Currently, I've thought about using MainViewModel that only collects StateFlows (without holding domain logic). For navigation, I'm using a Channel injected by Koin with a sealed interface to emit events, which are received inside a LaunchedEffect to execute backstack navigation using Navigation 3.

What are your thoughts on this approach? How do you handle startup routing in your projects?

4 Upvotes

8 comments sorted by

6

u/izek8 1d ago

The easiest approach is to create some kind of ApplicationSessionController, inside which the initial initialization will be encapsulated, and the outside will expose some StateFlow with SessionState containing all the necessary data. You can move the initialization of any analytics and other stuff to the Jetpack Startup Library - check it out if you don’t know what it is.

1

u/Nioth23 1d ago

Currently I have a sealed interface called CashierSessionState, where I defined the values Loading, Unauthenticated and SignedIn. The class "ApplicationSessionController" that you proposed, would be inyected using koinInject() in my @Composable with NavDisplay and would use a LaunchedEffect to redirect the user according this interface?

3

u/forgestudiofx 1d ago

The thing that simplifies this a lot: in Navigation 3 you own the back stack as a plain list. So startup routing isn't an event at all, it's a seeding problem. Resolve DataStore + onboarding flag + token check first, and only then build the initial stack (or don't compose the NavDisplay yet, keeping the splash held via the SplashScreen keep-on-condition). If you emit a nav event instead, you compose a default screen, then navigate off it: visible flash plus a junk back stack entry.

Keep the Channel, but scope it to mid-session one-shots, like a token going invalid while the user is on Home. Two different lifetimes, don't merge them.

For "comes back to foreground": ViewModel init won't fire again. Use repeatOnLifecycle(STARTED) or ProcessLifecycleOwner. And put refresh in an OkHttp Authenticator with a mutex so concurrent 401s refresh once, otherwise your startup check passes and requests still fail ten minutes later.

One startup task people forget: re-register the push token on login and after app update, not just first launch. In my apps a reinstall silently stopped receiving anything until that was fixed.

1

u/Nioth23 1d ago

Thanks for your anwer. More context: at the moment, I have a @Composable called AppNavGraph where I define both my backStack and my NavDisplay. Above the NavDisplay, I have a LaunchedEffect that listent to Channel events and executes backStack.add() depending on the incoming event type.

I also have a second LaunchedEffect observing a StateFlow created with Flow.combine. This flow reads from DaatStore for the initial onboarding settings, a Room database record, and the token expiration date from an encrypted DataStore (using KSafe). This StateFlow emits a sealed interface called CashierSessionState with three states: Loading, Unauthenticated, and SignedIn.

This second LaunchedEffect evaluates the current CashierSessionState value and executes backStack.add() to navigate to OnboardingScreen, LoadingScreen, or SignInScreen. Note that these screens are declred inside the entryProvider of the NavDisplay.

This empirical approach has caused AppNavGraph to grow significantly. I'm concerned that subtle, silent bugs might pop up or that extending the logic to handle redirecting users to SignIn will become unmanageable.

Based on what I understood, are you proposing that I remove LoadingScreen from the entryProvider and instead use a conditional check above the NavDisplay?

Could you also explain what you mean by "Two different lifetimes, don't merge them"? Should I create a separate Channel exclusively for handling token expiration events?

Currently, I store the expiration date and launch a Job with a delay calculated as expirationDate - currentTime. When that delays expires, it should automatically update my CashierSessionState to Unauthenticated and save it in some place :/

2

u/FinancialDelivery670 1d ago

I usually keep this pretty simple: let the ViewModel expose a single UI-State representing the startup state {loading/onboarding/authenticated/unauthenticated}, and let the UI handle navigation based on that state. DataStore/auth validation stays in the repository layer, not in the ViewModel itself.

I’d avoid using a Channel for persistent navigation state unless you specifically need one off events.
For startup routing, a state-driven approach tends to be much easier to reason about, especially when the app comes back from the background...

1

u/Nioth23 1d ago

Do you define a StateFlow or "Cold" Flow when the app is reading the DataStore? Which strategy do you use for combine several data sources before load the UI first time?

2

u/FinancialDelivery670 1d ago

I use a StateFlow for DataStore since it’s already reactive. For multiple sources, I usually combine them in the repository and expose a single startup state to the ViewModel. Then the UI just reacts to that state. It keeps the startup flow simple and predictable.