Cold-start navigation and authentication gates
The same link can arrive when the process is absent, the Activity exists, the graph is not ready, the user is signed out, or another account is active. Route it through one pipeline instead of scattering checks across Activities and composables.
Intent/notification/link
↓
parse + validate syntax
↓
ExternalDestination (typed)
↓
wait for app/session restoration
↓
authentication + authorization decision
↓
navigate once / show safe fallback
Preserve intent, not a back stack
data class PendingDestination(
val destination: ExternalDestination,
val receivedAt: Instant,
val nonce: String,
)
If login is needed, preserve the typed destination, complete authentication, then resolve it again under the current user. Do not persist raw Intent objects or blindly replay extras.
A coordinator
class ExternalRouteCoordinator(
private val session: SessionRepository,
private val orders: OrderRepository,
) {
suspend fun resolve(destination: ExternalDestination): RouteResult =
when (destination) {
is ExternalDestination.Order -> {
if (!session.isAuthenticated()) {
RouteResult.RequireLogin(destination)
} else if (orders.canOpen(destination.id)) {
RouteResult.Navigate(AppRoute.Order(destination.id))
} else {
RouteResult.Rejected("Order unavailable")
}
}
ExternalDestination.Unsupported -> RouteResult.Rejected("Unsupported link")
}
}
The UI observes one-shot route results and acknowledges consumption. A plain replaying StateFlow can navigate twice after configuration change unless the state includes explicit consumption/identity semantics.
Cold versus warm delivery
- Cold start: inspect the Activity’s initial intent after essential app/session initialization.
- Existing Activity: handle the new intent (
onNewIntent) and update the Activity’s stored intent if appropriate. - Compose Navigation: emit a typed route after the
NavHostis ready; avoid navigation directly during composition. - Multiple links: define whether to queue, replace, or deduplicate. A stable event ID prevents double handling.
Back-stack expectations
Opening an order should usually produce a sensible Up path (for example Orders → Order) while system Back returns to the previous app when appropriate. Build synthetic stacks only where product behavior requires them; do not manufacture confusing duplicate destinations.
Practical edge cases
- Link belongs to account A while account B is active: reject or offer explicit account switch; never leak metadata.
- Token expired during login: show a safe expired-link state, not the home screen with no explanation.
- User taps the same notification repeatedly: navigate idempotently or use single-top semantics.
- Resource deleted: show a stable “no longer available” destination.
- App initialization fails offline: retain a bounded pending request and expose retry/cancel.
Sources
Interview question
Where should authentication be checked? At routing for UX and on the server/data boundary for actual authorization. Navigation guards alone are not security boundaries.