Deferred Navigation is a pattern of delayed navigation where transitioning to the next screen occurs after an asynchronous operation completes, rather than directly at the moment of user action. According to Android Developers (2024), deferred navigation helps avoid race conditions between navigation and data loading, and simplifies handling transitions from push notifications and Deeplinks. The key difference is that the route is calculated after all necessary data is available.
Key Takeaways
Deferred Navigation is an architectural pattern where the navigation decision is postponed until all necessary data becomes available. Unlike a direct transition where the user presses a button and immediately lands on a new screen, deferred navigation separates the trigger event and the actual transition by placing an asynchronous operation between them.
Architecturally, Deferred Navigation is built on state change: a button press initiates an asynchronous process, and a subscription to its result triggers navigation. This is especially important in applications with MVVM or MVI architecture, where the ViewModel manages state and the View (Activity, Fragment, SwiftUI View) subscribes to changes and reacts with a transition. This approach eliminates the direct dependency between UI and navigation logic.
According to Google I/O 2023, deferred navigation is recommended for all scenarios where navigation depends on the result of a network request, authentication check, configuration loading, or permissions. The pattern is also mandatory when handling Deeplinks, where the app must first launch, load the root screen, and only then navigate to the target route.
Deferred Navigation is used in scenarios where direct navigation leads to an incorrect screen state or loading errors. Let us examine four main cases where deferred navigation is required.
If a user taps on protected content, the application must first verify the access token. Direct navigation to the content screen will result in an empty screen or a 401 error if the token has expired. Deferred navigation checks the token, and only on success — navigates to the target screen. On failure — redirects to the login screen.
When an application is opened via an external link, it must first load the root screen, restore the navigation state, and only then perform the Deeplink transition. Direct navigation to the target screen without root context will lead to anomalies: an empty navigation stack or a broken back stack.
When tapping on a push notification, the application can be in one of three states: closed, in the background, or active. Deferred Navigation determines the application state, loads the necessary content, and only then displays the target screen. iOS allows handling this scenario through UNNotificationContentExtension.
If a screen's functionality is controlled by a feature flag from the server, deferred navigation allows first requesting the configuration and only then showing the screen. If the feature is disabled — the user sees alternative content or a placeholder instead of an empty screen.
| Scenario | Direct Navigation | Deferred Navigation |
|---|---|---|
| Authorization | Empty screen with expired token | Redirect to login |
| Deeplink | Broken back stack | Correct navigation stack |
| Push | Loading without context | Data ready before transition |
| Feature flag | Displaying unavailable functionality | Placeholder or alternative |
Direct navigation is a traditional approach where the transition is performed immediately in response to an event. The user presses a button, and the UI router immediately switches the screen. This approach is simple and predictable but limited in scenarios that require data from the server or condition checks.
Deferred Navigation adds a layer in the form of asynchronous state. A user event initiates an operation, and a subscription to the result controls navigation. This increases code complexity but provides flexibility: the same trigger can lead to different screens depending on the loaded data.
The choice between the two approaches depends on requirements: if displaying a screen does not require asynchronous data — use direct navigation. If the screen depends on the result of a request, authorization, or external conditions — deferred navigation is required. A hybrid approach, where some transitions are direct and some are deferred, is the most common practice in industrial applications.
Android Jetpack provides mechanisms for implementing Deferred Navigation at the architecture level. The core idea is that the ViewModel manages state, while the Activity or Fragment subscribes to changes and triggers navigation through NavController.
StateFlow in Kotlin coroutines is the ideal tool for deferred navigation. The ViewModel updates a StateFlow with a navigation event, and the Activity observes it and performs the transition. Once the event is processed, the StateFlow is cleared, preventing repeated navigation.
class MainViewModel : ViewModel() {
private val _navigation = MutableSharedFlow<NavigationEvent>()
val navigation: SharedFlow<NavigationEvent> = _navigation
fun onDeepLinkReceived(link: String) {
viewModelScope.launch {
val data = resolveDeepLink(link)
_navigation.emit(NavigationEvent.GoToScreen(data))
}
}
}
In the Activity, a subscription to navigation triggers NavController with the route from the ViewModel. To prevent repeated navigation on screen rotation, a NavigationEventWrapper wrapper is used, which processes the event only once. Jetpack Navigation 2.7+ supports Safe Args for type-safe argument passing.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: MainViewModel by viewModels()
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.navigation.collect { event ->
when (event) {
is NavigationEvent.GoToScreen ->
findNavController(R.id.nav_host)
.navigate(event.route)
}
}
}
}
}
iOS does not have a built-in Navigation Component similar to Android Jetpack, so developers implement Deferred Navigation through the Coordinator Pattern in combination with Combine or async/await. The Coordinator manages the screen stack and makes navigation decisions based on loaded data.
Coordinator is an object that manages navigation between ViewControllers. In combination with Combine, the ViewModel publishes events through PassthroughSubject, and the Coordinator subscribes to them and performs the transition. This approach completely separates UI from navigation logic and follows Apple's recommendations for application architecture.
final class AppCoordinator {
private var cancellables = Set<AnyCancellable>()
func start(viewModel: MainViewModel) {
viewModel.$navigationDestination
.compactMap { $0 }
.sink { [weak self] destination in
self?.navigateTo(destination)
}
.store(in: &cancellables)
}
}
Swift 5.5 introduced structured concurrency, which allows implementing deferred navigation through async/await without Combine. The ViewModel provides an async function that returns a route after loading data. The Coordinator calls this function in a Task and performs the transition based on the received route.
class AuthViewModel: ObservableObject {
func resolveDeeplink(_ url: URL) async -> AppRoute? {
guard let token = await AuthService.shared.getValidToken() else { return .login }
return await DeeplinkRouter.resolve(url, token: token)
}
}
// In Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Deferred Navigation simplifies handling asynchronous scenarios but requires a disciplined approach to state management. Let us examine the main mistakes developers make when implementing deferred navigation.
The most common mistake is attempting to perform deferred navigation before the root screen is fully initialized and the NavController or Coordinator is ready for the transition. In Android this leads to IllegalStateException, in iOS — to an undefined UI state. The solution is to ensure the component lifecycle is in STARTED or RESUMED state before triggering navigation.
If the StateFlow or Subject does not clear the event after processing, when returning to the previous screen the user may be automatically redirected to the same screen again. Use SharedFlow with replay=0 on Android or CurrentValueSubject with nil after processing on iOS, so that the navigation event fires only once.
Use one central component for all navigation in the application. When each Activity, Fragment, or ViewController has its own navigation controller, deferred navigation between different parts of the application becomes chaotic. A single Coordinator simplifies debugging and testing of navigation scenarios.
Deferred Navigation is harder to test than direct navigation because asynchronous operations introduce a time factor. Use TestDispatcher in Android (kotlinx-coroutines-test) and XCTestExpectation in iOS to simulate data loading and verify that navigation follows the expected route. Mock authorization and deeplink services for isolated testing of each scenario.
Frequently Asked Questions
Deferred Navigation is a delayed transition pattern that can be applied in any asynchronous scenario. Deep Link is one trigger for deferred navigation, but not the only one. Authorization and feature flags also use delayed navigation.
Yes, most applications use a hybrid approach. A product list screen (without asynchronous dependencies) can use direct navigation, while a detail screen with data loading uses deferred. The separation is determined by the architecture of each specific screen.
Use SharedFlow without replay on Android and combineLatest without buffering on iOS. Cancel previous subscriptions on a new trigger. This ensures that only the latest navigation event is processed.
Yes, in Compose deferred navigation is implemented through subscription to ViewModel StateFlow and calling NavController.navigate in LaunchedEffect. Google recommends using Navigation Compose with an event-driven model for deferred scenarios.
Delay the navigation until the application becomes active again. In Android use Lifecycle.State.STARTED to filter events. In iOS check UIApplication.State in Combine or async/await blocks.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also