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.
Ако функционалността на екрана се управлява от feature flag от сървъра, deferred navigation позволява първо да се поиска конфигурация и едва след това да се покаже екранът. Ако функцията е изключена, потребителят вижда алтернативно съдържание или placeholder вместо празен екран.
| Сценарий | Директна навигация | Deferred Navigation |
|---|---|---|
| Авторизация | Празен екран при изтекъл токен | Пренасочване към вход |
| Deeplink | Счупен back stack | Правилен навигационен стек |
| Push | Зареждане без контекст | Данните готови преди прехода |
| Feature flag | Показване на недостъпна функция | Placeholder или алтернатива |
Директната навигация (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 абонаментът за навигация задейства 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 — обект, който управлява навигацията между ViewControllers. В комбинация с 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)
}
}
// В Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Deferred Navigation опростява обработката на асинхронни сценарии, но изисква дисциплиниран подход към управлението на състоянието. Нека разгледаме основните грешки, които разработчиците допускат при внедряване на 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, но не и единственият. Авторизацията и feature flag-овете също използват отложена навигация.
Да, повечето приложения използват хибриден подход. Екранът със списък на продукти (без асинхронни зависимости) може да използва директна навигация, а екранът с детайли и зареждане на данни — 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също