Deferred Navigation (Відкладена навігація) — це патерн відкладеної навігації, за якого перехід на наступний екран відбувається після завершення асинхронної операції, а не безпосередньо в момент дії користувача. За даними Android Developers (2024), відкладена навігація дозволяє уникнути стану гонки між навігацією та завантаженням даних, а також спрощує обробку переходів із push-повідомлень і Deeplink. Ключова відмінність — маршрут обчислюється після того, як усі необхідні дані доступні.
Головне
Deferred Navigation (Відкладена навігація) — це архітектурний патерн, за якого навігаційне рішення відкладається до моменту, коли стануть доступні всі необхідні дані. На відміну від прямого переходу, де користувач натискає кнопку та одразу потрапляє на новий екран, відкладена навігація розділяє подію-тригер і фактичний перехід, додаючи між ними асинхронну операцію.
Архітектурно Deferred Navigation будується на зміні стану: натискання кнопки ініціює асинхронний процес, а підписка на його результат запускає навігацію. Це особливо важливо в застосунках з архітектурою MVVM або MVI, де ViewModel керує станом, а View (Activity, Fragment, SwiftUI View) підписується на зміни та реагує переходом. Такий підхід усуває пряму залежність між UI та навігаційною логікою.
За даними Google I/O 2023, відкладена навігація рекомендована для всіх сценаріїв, де навігація залежить від результату мережевого запиту, перевірки авторизації, завантаження конфігурації або дозволів. Патерн також обов'язковий під час обробки Deeplink, коли застосунок має спочатку запуститися, завантажити кореневий екран і лише потім перейти на цільовий маршрут.
Deferred Navigation застосовується в сценаріях, де прямий перехід призводить до некоректного стану екрана або помилок завантаження. Розглянемо чотири основні випадки, коли відкладена навігація обов'язкова.
Якщо користувач натискає на захищений контент, застосунок має спочатку перевірити токен доступу. Пряма навігація на екран контенту призведе до порожнього екрана або помилки 401, якщо токен закінчився. Відкладена навігація перевіряє токен, і лише при успіху — переходить на цільовий екран. При невдачі — перенаправляє на екран входу.
Коли застосунок відкривається за зовнішнім посиланням, спочатку потрібно завантажити кореневий екран, відновити стан навігації та лише потім виконати перехід за Deeplink. Пряма навігація на цільовий екран без кореневого контексту призведе до аномалій: порожнього стеку навігації або зламаного back stack.
При тапі по push-повідомленню застосунок може перебувати в трьох станах: закритий, у фоні або активний. Deferred Navigation визначає стан застосунку, завантажує необхідний контент і лише потім показує цільовий екран. iOS дозволяє обробити цей сценарій через UNNotificationContentExtension.
Якщо функціональність екрана керується фіче-флагом із сервера, відкладена навігація дозволяє спочатку запитати конфігурацію та лише потім показати екран. Якщо фіча вимкнена — користувач бачить альтернативний контент або заглушку замість порожнього екрана.
| Сценарій | Пряма навігація | Deferred Navigation |
|---|---|---|
| Авторизація | Порожній екран при простроченому токені | Перенаправлення на логін |
| Deeplink | Зламаний back stack | Коректний стек навігації |
| Push | Завантаження без контексту | Дані готові до переходу |
| Фіче-флаг | Відображення недоступної функції | Заглушка або альтернатива |
Пряма навігація — традиційний підхід, за якого перехід виконується негайно у відповідь на подію. Користувач натискає кнопку, і UI роутер одразу перемикає екран. Цей підхід простий і передбачуваний, але обмежений у сценаріях, де потрібні дані з сервера або перевірка умов.
Deferred Navigation додає прошарок у вигляді асинхронного стану. Подія користувача запускає операцію, а підписка на результат керує навігацією. Це збільшує складність коду, але дає гнучкість: один і той самий тригер може вести на різні екрани залежно від завантажених даних.
Вибір між двома підходами залежить від вимог: якщо для відображення екрана не потрібні асинхронні дані — використовуйте пряму навігацію. Якщо екран залежить від результату запиту, авторизації або зовнішніх умов — відкладена навігація обов'язкова. Гібридний підхід, де частина переходів пряма, а частина — відкладена, є найпоширенішою практикою в промислових застосунках.
Android Jetpack надає механізми для реалізації Deferred Navigation на рівні архітектури. Основна ідея — ViewModel керує станом, а Activity або Fragment підписується на зміни та запускає навігацію через NavController.
StateFlow у Kotlin корутинах — ідеальний інструмент для відкладеної навігації. 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 підписка на навігацію запускає 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 спрощує обробку асинхронних сценаріїв, але вимагає дисциплінованого підходу до керування станом. Розглянемо основні помилки, яких припускаються розробники під час впровадження відкладеної навігації.
Найчастіша помилка — спроба виконати відкладену навігацію до того, як кореневий екран повністю ініціалізовано та NavController або Coordinator готовий до переходу. В Android це призводить до IllegalStateException, в iOS — до невизначеного стану UI. Рішення — переконатися, що життєвий цикл компонента перебуває в стані STARTED або RESUMED перед запуском навігації.
Якщо StateFlow або Subject не очищає подію після обробки, при поверненні на попередній екран користувач може бути автоматично перенаправлений на той самий екран знову. Використовуйте SharedFlow з replay=0 на Android або CurrentValueSubject з nil після обробки на iOS, щоб подія навігації спрацьовувала лише один раз.
Використовуйте один центральний компонент для всієї навігації в застосунку. Коли кожна Activity, Fragment або ViewController має власний навігаційний контролер, відкладена навігація між різними частинами застосунку стає хаотичною. Єдиний Coordinator полегшує налагодження та тестування навігаційних сценаріїв.
Deferred Navigation складніше тестувати, ніж пряму, оскільки асинхронні операції вносять фактор часу. Використовуйте TestDispatcher в Android (kotlinx-coroutines-test) та XCTestExpectation в iOS для симуляції завантаження даних і перевірки, що навігація виконується за очікуваним маршрутом. Мокайте сервіси авторизації та деплінків для ізольованого тестування кожного сценарію.
Часті запитання
Deferred Navigation — це патерн відкладеного переходу, який може застосовуватися в будь-яких асинхронних сценаріях. Deep Link — один із тригерів для відкладеної навігації, але не єдиний. Авторизація та фіче-флаги теж використовують відкладену навігацію.
Так, більшість застосунків використовують гібридний підхід. Екран списку товарів (без асинхронних залежностей) може використовувати пряму навігацію, а екран деталей із завантаженням даних — deferred. Розділення визначається архітектурою конкретного екрана.
Використовуйте SharedFlow без replay на Android та combineLatest без буферизації на iOS. Скасовуйте попередні підписки при новому тригері. Це гарантує, що обробляється лише остання навігаційна подія.
Так, в Compose відкладена навігація реалізується через підписку на StateFlow ViewModel та виклик NavController.navigate в LaunchedEffect. Google рекомендує використовувати Navigation Compose з подійною моделлю для deferred сценаріїв.
Відкладіть навігацію до моменту, коли застосунок знову стане активним. В Android використовуйте Lifecycle.State.STARTED для фільтрації подій. В iOS перевірте UIApplication.State в Combine або async/await блоці.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.