Deferred Navigation — это паттерн отложенной навигации, при котором переход на следующий экран происходит после завершения асинхронной операции, а не непосредственно в момент действия пользователя. По данным Android Developers (2024), deferred navigation позволяет избежать состояния гонки между навигацией и загрузкой данных, а также упрощает обработку переходов из push-уведомлений и Deeplink. Ключевое отличие — маршрут вычисляется после того, как все необходимые данные доступны.
Главное
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. Прямая навигация на целевой экран без корневого контекста приведёт к аномалиям: пустому стеку навигации или сломанному back stack.
При тапе по push-уведомлению приложение может находиться в трёх состояниях: закрыто, в фоне или активно. Deferred Navigation определяет состояние приложения, загружает необходимый контент и только затем показывает целевой экран. iOS позволяет обработать этот сценарий через UNNotificationContentExtension.
Если функциональность экрана управляется фиче-флагом с сервера, deferred navigation позволяет сначала запросить конфигурацию и только потом показать экран. Если фича отключена — пользователь видит альтернативный контент или заглушку вместо пустого экрана.
| Сценарий | Прямая навигация | Deferred Navigation |
|---|---|---|
| Авторизация | Пустой экран при истёкшем токене | Перенаправление на логин |
| Deeplink | Сломанный back stack | Корректный стек навигации |
| Push | Загрузка без контекста | Данные готовы до перехода |
| Фиче-флаг | Отображение недоступной функции | Заглушка или альтернатива |
Прямая навигация (direct navigation) — традиционный подход, при котором переход выполняется немедленно в ответ на событие. Пользователь нажимает кнопку, и UI роутер сразу переключает экран. Этот подход прост и предсказуем, но ограничен в сценариях, где нужны данные с сервера или проверка условий.
Deferred Navigation добавляет прослойку в виде асинхронного состояния. Событие пользователя запускает операцию, а подписка на результат управляет навигацией. Это увеличивает сложность кода, но даёт гибкость: один и тот же триггер может вести на разные экраны в зависимости от загруженных данных.
Выбор между двумя подходами зависит от требований: если для отображения экрана не требуется асинхронных данных — используйте прямую навигацию. Если экран зависит от результата запроса, авторизации или внешних условий — deferred navigation обязательна. Гибридный подход, где часть переходов прямая, а часть — отложенная, — наиболее распространённая практика в промышленных приложениях.
Android Jetpack предоставляет механизмы для реализации Deferred Navigation на уровне архитектуры. Основная идея — ViewModel управляет состоянием, а Activity или Fragment подписывается на изменения и запускает навигацию через NavController.
StateFlow в Kotlin корутинах — идеальный инструмент для deferred navigation. ViewModel обновляет StateFlow с навигационным событием, а Activity наблюдает за ним и выполняет переход. Когда событие обработано, StateFlow очищается, предотвращая повторную навигацию.
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))
}
}
}
В Activity подписка на navigation запускает NavController с маршрутом из ViewModel. Для предотвращения повторной навигации при повороте экрана используется обёртка NavigationEventWrapper, которая обрабатывает событие только один раз. Jetpack Navigation 2.7+ поддерживает Safe Args для типобезопасной передачи аргументов.
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 не имеет встроенного Navigation Component, аналогичного Android Jetpack, поэтому разработчики реализуют Deferred Navigation через Coordinator Pattern в связке с Combine или async/await. Coordinator управляет стеком экранов и принимает решения о навигации на основе загруженных данных.
Coordinator — объект, управляющий навигацией между ViewController. В связке с Combine, ViewModel публикует события через PassthroughSubject, а Coordinator подписывается на них и выполняет переход. Такой подход полностью отделяет UI от навигационной логики и соответствует рекомендациям Apple по архитектуре приложений.
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 ввёл structured concurrency, который позволяет реализовать отложенную навигацию через async/await без Combine. ViewModel предоставляет async-функцию, которая возвращает маршрут после загрузки данных. Coordinator вызывает эту функцию в Task и выполняет переход по полученному маршруту.
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 упрощает обработку асинхронных сценариев, но требует disciplined подхода к управлению состоянием. Рассмотрим основные ошибки, которые допускают разработчики при внедрении deferred navigation.
Самая частая ошибка — попытка выполнить deferred navigation до того, как корневой экран полностью инициализирован и NavController или Coordinator готов к переходу. В Android это приводит к IllegalStateException, в iOS — к неопределённому состоянию UI. Решение — убедиться, что жизненный цикл компонента находится в состоянии STARTED или RESUMED перед запуском навигации.
Если StateFlow или Subject не очищает событие после обработки, при возврате на предыдущий экран пользователь может быть автоматически перенаправлен на тот же экран снова. Используйте SharedFlow с replay=0 на Android или CurrentValueSubject с nil после обработки на iOS, чтобы событие навигации срабатывало только один раз.
Используйте один центральный компонент для всей навигации в приложении. Когда каждая Activity, Fragment или ViewController имеет собственный навигационный контроллер, deferred navigation между разными частями приложения становится хаотичной. Единый Coordinator облегчает отладку и тестирование навигационных сценариев.
Deferred Navigation сложнее тестировать, чем прямую, поскольку асинхронные операции вносят фактор времени. Используйте TestDispatcher в Android (kotlinx-coroutines-test) и XCTestExpectation в iOS для симуляции загрузки данных и проверки, что навигация выполняется по ожидаемому маршруту. Мокайте сервисы авторизации и деплинков для изолированного тестирования каждого сценария.
Часто задаваемые вопросы
Deferred Navigation — это паттерн отложенного перехода, который может применяться в любых асинхронных сценариях. Deep Link — один из триггеров для deferred navigation, но не единственный. Авторизация и фиче-флаги тоже используют отложенную навигацию.
Да, большинство приложений используют гибридный подход. Экран списка товаров (без асинхронных зависимостей) может использовать прямую навигацию, а экран деталей с загрузкой данных — deferred. Разделение определяется архитектурой конкретного экрана.
Используйте SharedFlow без replay на Android и combineLatest без буферизации на iOS. Отменяйте предыдущие подписки при новом триггере. Это гарантирует, что обрабатывается только последнее навигационное событие.
Да, в Compose deferred navigation реализуется через подписку на StateFlow ViewModel и вызов NavController.navigate в LaunchedEffect. Google рекомендует использовать Navigation Compose с событийной моделью для deferred сценариев.
Отложите навигацию до момента, когда приложение снова станет активным. В Android используйте Lifecycle.State.STARTED для фильтрации событий. В iOS проверьте UIApplication.State в Combine или async/await блоке.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также