Deferred Navigation — что это, принципы и реализация

Автор: IT Sectr Опубликовано: 2026-06-10 Время чтения: 9 мин

Deferred Navigation — это паттерн отложенной навигации, при котором переход на следующий экран происходит после завершения асинхронной операции, а не непосредственно в момент действия пользователя. По данным Android Developers (2024), deferred navigation позволяет избежать состояния гонки между навигацией и загрузкой данных, а также упрощает обработку переходов из push-уведомлений и Deeplink. Ключевое отличие — маршрут вычисляется после того, как все необходимые данные доступны.

Главное

  • Deferred Navigation — отложенная навигация, где переход выполняется после завершения асинхронных операций
  • Прямая навигация обрабатывает переход мгновенно, deferred — дожидается данных
  • Типичные сценарии: авторизация, Deeplink, push-уведомления, загрузка конфигурации
  • В Android реализуется через Navigation Component с колбэками и StateFlow
  • В iOS используется Combine или async/await с координатором навигации

Что такое Deferred Navigation

Deferred Navigation — это архитектурный паттерн, при котором навигационное решение откладывается до момента, когда станут доступны все необходимые данные. В отличие от прямого перехода, где пользователь нажимает кнопку и сразу попадает на новый экран, deferred navigation разделяет событие-триггер и фактический переход, добавляя между ними асинхронную операцию.

Архитектурно Deferred Navigation строится на изменении состояния: нажатие кнопки инициирует асинхронный процесс, а подписка на его результат запускает навигацию. Это особенно важно в приложениях с архитектурой MVVM или MVI, где ViewModel управляет состоянием, а View (Activity, Fragment, SwiftUI View) подписывается на изменения и реагирует переходом. Такой подход устраняет прямую зависимость между UI и навигационной логикой.

По данным Google I/O 2023, deferred navigation рекомендована для всех сценариев, где навигация зависит от результата сетевого запроса, проверки авторизации, загрузки конфигурации или разрешений. Паттерн также обязателен при обработке Deeplink, когда приложение должно сначала запуститься, загрузить корневой экран и только затем перейти на целевой маршрут.

Когда нужна отложенная навигация

Deferred Navigation применяется в сценариях, где прямой переход приводит к некорректному состоянию экрана или ошибкам загрузки. Рассмотрим четыре основных случая, когда deferred navigation обязательна.

Авторизация и аутентификация

Если пользователь нажимает на защищённый контент, приложение должно сначала проверить токен доступа. Прямая навигация на экран контента приведёт к пустому экрану или ошибке 401, если токен истёк. Deferred navigation проверяет токен, и только при успехе — переходит на целевой экран. При неудаче — перенаправляет на экран входа.

Обработка Deeplink

Когда приложение открывается по внешней ссылке, сначала нужно загрузить корневой экран, восстановить состояние навигации и только затем выполнить переход по Deeplink. Прямая навигация на целевой экран без корневого контекста приведёт к аномалиям: пустому стеку навигации или сломанному back stack.

Push-уведомления с контентом

При тапе по push-уведомлению приложение может находиться в трёх состояниях: закрыто, в фоне или активно. Deferred Navigation определяет состояние приложения, загружает необходимый контент и только затем показывает целевой экран. iOS позволяет обработать этот сценарий через UNNotificationContentExtension.

Динамический фиче-флаг

Если функциональность экрана управляется фиче-флагом с сервера, deferred navigation позволяет сначала запросить конфигурацию и только потом показать экран. Если фича отключена — пользователь видит альтернативный контент или заглушку вместо пустого экрана.

СценарийПрямая навигацияDeferred Navigation
АвторизацияПустой экран при истёкшем токенеПеренаправление на логин
DeeplinkСломанный back stackКорректный стек навигации
PushЗагрузка без контекстаДанные готовы до перехода
Фиче-флагОтображение недоступной функцииЗаглушка или альтернатива

Deferred Navigation vs прямая навигация

Прямая навигация (direct navigation) — традиционный подход, при котором переход выполняется немедленно в ответ на событие. Пользователь нажимает кнопку, и UI роутер сразу переключает экран. Этот подход прост и предсказуем, но ограничен в сценариях, где нужны данные с сервера или проверка условий.

Deferred Navigation добавляет прослойку в виде асинхронного состояния. Событие пользователя запускает операцию, а подписка на результат управляет навигацией. Это увеличивает сложность кода, но даёт гибкость: один и тот же триггер может вести на разные экраны в зависимости от загруженных данных.

Выбор между двумя подходами зависит от требований: если для отображения экрана не требуется асинхронных данных — используйте прямую навигацию. Если экран зависит от результата запроса, авторизации или внешних условий — deferred navigation обязательна. Гибридный подход, где часть переходов прямая, а часть — отложенная, — наиболее распространённая практика в промышленных приложениях.

Реализация в Android: Navigation Component и ViewModel

Android Jetpack предоставляет механизмы для реализации Deferred Navigation на уровне архитектуры. Основная идея — ViewModel управляет состоянием, а Activity или Fragment подписывается на изменения и запускает навигацию через NavController.

Deferred Navigation с StateFlow

StateFlow в Kotlin корутинах — идеальный инструмент для deferred navigation. ViewModel обновляет StateFlow с навигационным событием, а Activity наблюдает за ним и выполняет переход. Когда событие обработано, StateFlow очищается, предотвращая повторную навигацию.

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 с NavController

В Activity подписка на navigation запускает NavController с маршрутом из ViewModel. Для предотвращения повторной навигации при повороте экрана используется обёртка NavigationEventWrapper, которая обрабатывает событие только один раз. Jetpack Navigation 2.7+ поддерживает Safe Args для типобезопасной передачи аргументов.

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

Реализация в iOS: Coordinator Pattern и Combine

iOS не имеет встроенного Navigation Component, аналогичного Android Jetpack, поэтому разработчики реализуют Deferred Navigation через Coordinator Pattern в связке с Combine или async/await. Coordinator управляет стеком экранов и принимает решения о навигации на основе загруженных данных.

Coordinator с Combine

Coordinator — объект, управляющий навигацией между ViewController. В связке с Combine, ViewModel публикует события через PassthroughSubject, а Coordinator подписывается на них и выполняет переход. Такой подход полностью отделяет UI от навигационной логики и соответствует рекомендациям Apple по архитектуре приложений.

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 с async/await

Swift 5.5 ввёл structured concurrency, который позволяет реализовать отложенную навигацию через async/await без Combine. ViewModel предоставляет async-функцию, которая возвращает маршрут после загрузки данных. Coordinator вызывает эту функцию в Task и выполняет переход по полученному маршруту.

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

Типичные ошибки и best practices

Deferred Navigation упрощает обработку асинхронных сценариев, но требует disciplined подхода к управлению состоянием. Рассмотрим основные ошибки, которые допускают разработчики при внедрении deferred navigation.

Ошибка: навигация до завершения инициализации

Самая частая ошибка — попытка выполнить deferred navigation до того, как корневой экран полностью инициализирован и NavController или Coordinator готов к переходу. В Android это приводит к IllegalStateException, в iOS — к неопределённому состоянию UI. Решение — убедиться, что жизненный цикл компонента находится в состоянии STARTED или RESUMED перед запуском навигации.

Ошибка: повторная навигация при возврате на экран

Если StateFlow или Subject не очищает событие после обработки, при возврате на предыдущий экран пользователь может быть автоматически перенаправлен на тот же экран снова. Используйте SharedFlow с replay=0 на Android или CurrentValueSubject с nil после обработки на iOS, чтобы событие навигации срабатывало только один раз.

Best Practice: единый NavController или Coordinator

Используйте один центральный компонент для всей навигации в приложении. Когда каждая Activity, Fragment или ViewController имеет собственный навигационный контроллер, deferred navigation между разными частями приложения становится хаотичной. Единый Coordinator облегчает отладку и тестирование навигационных сценариев.

Best Practice: тестирование сценариев отложенной навигации

Deferred Navigation сложнее тестировать, чем прямую, поскольку асинхронные операции вносят фактор времени. Используйте TestDispatcher в Android (kotlinx-coroutines-test) и XCTestExpectation в iOS для симуляции загрузки данных и проверки, что навигация выполняется по ожидаемому маршруту. Мокайте сервисы авторизации и деплинков для изолированного тестирования каждого сценария.

Часто задаваемые вопросы

Чем Deferred Navigation отличается от Deep Link?

Deferred Navigation — это паттерн отложенного перехода, который может применяться в любых асинхронных сценариях. Deep Link — один из триггеров для deferred navigation, но не единственный. Авторизация и фиче-флаги тоже используют отложенную навигацию.

Можно ли комбинировать deferred и direct navigation?

Да, большинство приложений используют гибридный подход. Экран списка товаров (без асинхронных зависимостей) может использовать прямую навигацию, а экран деталей с загрузкой данных — deferred. Разделение определяется архитектурой конкретного экрана.

Как избежать race condition при deferred navigation?

Используйте SharedFlow без replay на Android и combineLatest без буферизации на iOS. Отменяйте предыдущие подписки при новом триггере. Это гарантирует, что обрабатывается только последнее навигационное событие.

Поддерживает ли Jetpack Compose deferred navigation?

Да, в Compose deferred navigation реализуется через подписку на StateFlow ViewModel и вызов NavController.navigate в LaunchedEffect. Google рекомендует использовать Navigation Compose с событийной моделью для deferred сценариев.

Как обработать deferred navigation при свернутом приложении?

Отложите навигацию до момента, когда приложение снова станет активным. В Android используйте Lifecycle.State.STARTED для фильтрации событий. В iOS проверьте UIApplication.State в Combine или async/await блоке.

Итоги

  • Deferred Navigation — паттерн, при котором переход выполняется после завершения асинхронной операции
  • Основные сценарии: авторизация, Deeplink, push-уведомления, фиче-флаги
  • StateFlow/SharedFlow в Android и Combine/async-await в iOS — инструменты реализации
  • Deferred Navigation в отличие от прямой не вызывает состояние гонки при асинхронной загрузке
  • Navigation Component в Android и Coordinator Pattern в iOS — базовые архитектуры
  • Единый NavController/Coordinator обязателен для консистентной навигации
  • Тестирование deferred navigation требует TestDispatcher и моков для асинхронных сервисов

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также