Deferred Navigation — What It Is, Principles and Implementation

Author: IT Sectr Published: 2026-06-10 Reading time: 9 min

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 delayed navigation where the transition occurs after asynchronous operations complete
  • Direct navigation handles the transition instantly, deferred waits for data
  • Typical scenarios: authentication, Deeplink, push notifications, configuration loading
  • In Android it is implemented via Navigation Component with callbacks and StateFlow
  • In iOS Combine or async/await with a navigation coordinator is used

What Is Deferred Navigation

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.

When to Use Deferred Navigation

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.

Authorization and Authentication

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.

Handling Deeplinks

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.

Push Notifications with Content

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.

Dynamic Feature Flag

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.

ScenarioDirect NavigationDeferred Navigation
AuthorizationEmpty screen with expired tokenRedirect to login
DeeplinkBroken back stackCorrect navigation stack
PushLoading without contextData ready before transition
Feature flagDisplaying unavailable functionalityPlaceholder or alternative

Deferred Navigation vs Direct Navigation

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.

Implementation in Android: Navigation Component and ViewModel

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.

Deferred Navigation with StateFlow

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.

kotlin
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))
        }
    }
}

Deferred Navigation with NavController

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.

kotlin
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)
                }
            }
        }
    }
}

Implementation in iOS: Coordinator Pattern and Combine

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 with Combine

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.

swift
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)
    }
}

Deferred Navigation with async/await

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.

swift
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)
    }
}

Common Mistakes and Best Practices

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.

Mistake: navigation before initialization completes

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.

Mistake: repeated navigation when returning to a screen

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.

Best Practice: single NavController or Coordinator

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.

Best Practice: testing deferred 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

How is Deferred Navigation different from Deep Link?

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.

Can deferred and direct navigation be combined?

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.

How to avoid race conditions with deferred navigation?

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.

Does Jetpack Compose support deferred navigation?

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.

How to handle deferred navigation when the app is minimized?

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

  • Deferred Navigation is a pattern where the transition occurs after an asynchronous operation completes
  • Main scenarios: authentication, Deeplink, push notifications, feature flags
  • StateFlow/SharedFlow in Android and Combine/async-await in iOS are implementation tools
  • Deferred Navigation unlike direct navigation does not cause race conditions during async loading
  • Navigation Component in Android and Coordinator Pattern in iOS are the base architectures
  • A single NavController/Coordinator is mandatory for consistent navigation
  • Testing deferred navigation requires TestDispatcher and mocks for async services

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.

Discuss the project

Read also