Deferred Navigation to wzorzec odłożonej nawigacji, w którym przejście do następnego ekranu następuje po zakończeniu operacji asynchronicznej, a nie bezpośrednio w momencie działania użytkownika. Według Android Developers (2024), deferred navigation pozwala uniknąć stanu wyścigu między nawigacją a ładowaniem danych oraz upraszcza obsługę przejść z powiadomień push i Deeplink. Kluczowa różnica — trasa jest obliczana po uzyskaniu wszystkich niezbędnych danych.
Najważniejsze
Deferred Navigation to wzorzec architektoniczny, w którym decyzja nawigacyjna jest odkładana do momentu, gdy wszystkie niezbędne dane staną się dostępne. W przeciwieństwie do bezpośredniego przejścia, gdzie użytkownik klika przycisk i natychmiast trafia na nowy ekran, deferred navigation rozdziela zdarzenie-wyzwalacz i faktyczne przejście, dodając między nimi operację asynchroniczną.
Architektonicznie Deferred Navigation opiera się na zmianie stanu: kliknięcie przycisku inicjuje proces asynchroniczny, a subskrypcja jego wyniku uruchamia nawigację. Jest to szczególnie ważne w aplikacjach z architekturą MVVM lub MVI, gdzie ViewModel zarządza stanem, a View (Activity, Fragment, SwiftUI View) subskrybuje zmiany i reaguje przejściem. Takie podejście eliminuje bezpośrednią zależność między UI a logiką nawigacji.
Według danych Google I/O 2023, deferred navigation jest zalecana dla wszystkich scenariuszy, gdzie nawigacja zależy od wyniku zapytania sieciowego, weryfikacji autoryzacji, ładowania konfiguracji lub uprawnień. Wzorzec jest również obowiązkowy przy obsłudze Deeplink, gdy aplikacja musi najpierw uruchomić się, załadować ekran główny i dopiero przejść do docelowej trasy.
Deferred Navigation jest stosowana w scenariuszach, gdzie bezpośrednie przejście prowadzi do nieprawidłowego stanu ekranu lub błędów ładowania. Rozważmy cztery główne przypadki, gdy deferred navigation jest obowiązkowa.
Jeśli użytkownik klika na chronioną treść, aplikacja musi najpierw sprawdzić token dostępu. Nawigacja bezpośrednia na ekran treści doprowadzi do pustego ekranu lub błędu 401, jeśli token wygasł. Deferred navigation sprawdza token i tylko w przypadku powodzenia — przechodzi do docelowego ekranu. W przypadku niepowodzenia — przekierowuje do ekranu logowania.
Gdy aplikacja jest otwierana przez link zewnętrzny, najpierw trzeba załadować ekran główny, przywrócić stan nawigacji i dopiero wykonać przejście przez Deeplink. Bezpośrednia nawigacja na docelowy ekran bez kontekstu głównego doprowadzi do anomalii: pustego stosu nawigacji lub uszkodzonego back stack.
Po tapnięciu w powiadomienie push aplikacja może znajdować się w trzech stanach: zamknięta, w tle lub aktywna. Deferred Navigation określa stan aplikacji, ładuje niezbędną treść i dopiero wyświetla docelowy ekran. iOS umożliwia obsługę tego scenariusza przez UNNotificationContentExtension.
Jeśli funkcjonalność ekranu jest zarządzana przez feature flag z serwera, deferred navigation pozwala najpierw zapytać o konfigurację i dopiero pokazać ekran. Jeśli funkcja jest wyłączona — użytkownik widzi alternatywną treść lub zastępczy widok zamiast pustego ekranu.
| Scenariusz | Nawigacja bezpośrednia | Deferred Navigation |
|---|---|---|
| Autoryzacja | Pusty ekran przy wygasłym tokenie | Przekierowanie do logowania |
| Deeplink | Uszkodzony back stack | Prawidłowy stos nawigacji |
| Push | Ładowanie bez kontekstu | Dane gotowe przed przejściem |
| Feature flag | Wyświetlanie niedostępnej funkcji | Zastępczy widok lub alternatywa |
Nawigacja bezpośrednia (direct navigation) — tradycyjne podejście, w którym przejście jest wykonywane natychmiast w odpowiedzi na zdarzenie. Użytkownik klika przycisk, a router UI od razu przełącza ekran. To podejście jest proste i przewidywalne, ale ograniczone w scenariuszach, gdzie potrzebne są dane z serwera lub sprawdzenie warunków.
Deferred Navigation dodaje warstwę w postaci stanu asynchronicznego. Zdarzenie użytkownika uruchamia operację, a subskrypcja wyniku zarządza nawigacją. Zwiększa to złożoność kodu, ale daje elastyczność: ten sam wyzwalacz może prowadzić do różnych ekranów w zależności od załadowanych danych.
Wybór między dwoma podejściami zależy od wymagań: jeśli do wyświetlenia ekranu nie są potrzebne dane asynchroniczne — używaj nawigacji bezpośredniej. Jeśli ekran zależy od wyniku zapytania, autoryzacji lub warunków zewnętrznych — deferred navigation jest obowiązkowa. Podejście hybrydowe, gdzie część przejść jest bezpośrednia, a część odłożona, to najczęstsza praktyka w aplikacjach produkcyjnych.
Android Jetpack zapewnia mechanizmy do implementacji Deferred Navigation na poziomie architektury. Główna idea — ViewModel zarządza stanem, a Activity lub Fragment subskrybuje zmiany i uruchamia nawigację przez NavController.
StateFlow w Kotlin coroutines — idealne narzędzie do deferred navigation. ViewModel aktualizuje StateFlow ze zdarzeniem nawigacyjnym, a Activity obserwuje je i wykonuje przejście. Gdy zdarzenie jest obsłużone, StateFlow jest czyszczony, zapobiegając ponownej nawigacji.
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))
}
}
}
W Activity subskrypcja nawigacji uruchamia NavController z trasą z ViewModel. Aby zapobiec ponownej nawigacji przy obrocie ekranu, używany jest wrapper NavigationEventWrapper, który obsługuje zdarzenie tylko raz. Jetpack Navigation 2.7+ wspiera Safe Args dla bezpiecznego typowo przekazywania argumentów.
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 nie ma wbudowanego Navigation Component analogicznego do Android Jetpack, dlatego programiści implementują Deferred Navigation przez Coordinator Pattern w połączeniu z Combine lub async/await. Coordinator zarządza stosem ekranów i podejmuje decyzje nawigacyjne na podstawie załadowanych danych.
Coordinator — obiekt zarządzający nawigacją między ViewController. W połączeniu z Combine, ViewModel publikuje zdarzenia przez PassthroughSubject, a Coordinator subskrybuje je i wykonuje przejście. Takie podejście całkowicie oddziela UI od logiki nawigacji i jest zgodne z zaleceniami Apple dotyczącymi architektury aplikacji.
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 wprowadził structured concurrency, który umożliwia implementację odłożonej nawigacji przez async/await bez Combine. ViewModel udostępnia funkcję async, która zwraca trasę po załadowaniu danych. Coordinator wywołuje tę funkcję w Task i wykonuje przejście według otrzymanej trasy.
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)
}
}
// W Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
Deferred Navigation upraszcza obsługę scenariuszy asynchronicznych, ale wymaga zdyscyplinowanego podejścia do zarządzania stanem. Rozważmy główne błędy, które popełniają programiści przy wdrażaniu deferred navigation.
Najczęstszy błąd — próba wykonania deferred navigation zanim ekran główny zostanie w pełni zainicjalizowany, a NavController lub Coordinator będzie gotowy do przejścia. W Android prowadzi to do IllegalStateException, w iOS — do nieokreślonego stanu UI. Rozwiązanie — upewnij się, że cykl życia komponentu znajduje się w stanie STARTED lub RESUMED przed uruchomieniem nawigacji.
Jeśli StateFlow lub Subject nie czyści zdarzenia po obsłużeniu, przy powrocie do poprzedniego ekranu użytkownik może zostać automatycznie przekierowany na ten sam ekran ponownie. Używaj SharedFlow z replay=0 na Android lub CurrentValueSubject z nil po obsłużeniu na iOS, aby zdarzenie nawigacji uruchamiało się tylko raz.
Używaj jednego centralnego komponentu do całej nawigacji w aplikacji. Gdy każde Activity, Fragment lub ViewController ma własny kontroler nawigacji, deferred navigation między różnymi częściami aplikacji staje się chaotyczna. Jeden Coordinator ułatwia debugowanie i testowanie scenariuszy nawigacyjnych.
Deferred Navigation jest trudniejsza do testowania niż bezpośrednia, ponieważ operacje asynchroniczne wprowadzają czynnik czasu. Używaj TestDispatcher w Android (kotlinx-coroutines-test) i XCTestExpectation w iOS do symulacji ładowania danych i sprawdzenia, że nawigacja wykonuje się według oczekiwanej trasy. Mockuj serwisy autoryzacji i deeplinków do izolowanego testowania każdego scenariusza.
Często zadawane pytania
Deferred Navigation to wzorzec odłożonego przejścia, który może być stosowany w każdym scenariuszu asynchronicznym. Deep Link to jeden z wyzwalaczy dla deferred navigation, ale nie jedyny. Autoryzacja i feature flagi również używają odłożonej nawigacji.
Tak, większość aplikacji używa podejścia hybrydowego. Ekran listy produktów (bez zależności asynchronicznych) może używać nawigacji bezpośredniej, a ekran szczegółów z ładowaniem danych — deferred. Podział określa architektura konkretnego ekranu.
Używaj SharedFlow bez replay na Android i combineLatest bez buforowania na iOS. Anuluj poprzednie subskrypcje przy nowym wyzwalaczu. To gwarantuje, że obsługiwane jest tylko ostatnie zdarzenie nawigacyjne.
Tak, w Compose deferred navigation jest implementowana przez subskrypcję StateFlow ViewModel i wywołanie NavController.navigate w LaunchedEffect. Google zaleca używanie Navigation Compose z modelem zdarzeniowym dla scenariuszy deferred.
Odłóż nawigację do momentu, gdy aplikacja znów stanie się aktywna. W Android używaj Lifecycle.State.STARTED do filtrowania zdarzeń. W iOS sprawdź UIApplication.State w Combine lub async/await.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również