Deferred Navigation — co to jest, zasady i implementacja

Autor: IT Sectr Opublikowano: 2026-06-10 Czas czytania: 9 min

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 — odłożona nawigacja, gdzie przejście jest wykonywane po zakończeniu operacji asynchronicznych
  • Nawigacja bezpośrednia obsługuje przejście natychmiast, deferred — czeka na dane
  • Typowe scenariusze: autoryzacja, Deeplink, powiadomienia push, ładowanie konfiguracji
  • W Android realizowana przez Navigation Component z callbackami i StateFlow
  • W iOS używany jest Combine lub async/await z koordynatorem nawigacji

Co to jest Deferred Navigation

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.

Kiedy potrzebna jest odłożona nawigacja

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.

Autoryzacja i uwierzytelnianie

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.

Obsługa Deeplink

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.

Powiadomienia push z treścią

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.

Dynamiczny feature flag

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.

ScenariuszNawigacja bezpośredniaDeferred Navigation
AutoryzacjaPusty ekran przy wygasłym tokeniePrzekierowanie do logowania
DeeplinkUszkodzony back stackPrawidłowy stos nawigacji
PushŁadowanie bez kontekstuDane gotowe przed przejściem
Feature flagWyświetlanie niedostępnej funkcjiZastępczy widok lub alternatywa

Deferred Navigation vs nawigacja bezpośrednia

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.

Implementacja w Android: Navigation Component i ViewModel

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.

Deferred Navigation z StateFlow

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.

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

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.

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

Implementacja w iOS: Coordinator Pattern i Combine

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 z Combine

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.

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

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.

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

// W Coordinator:
Task {
    if let route = await viewModel.resolveDeeplink(url) {
        navigateTo(route)
    }
}

Typowe błędy i best practices

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.

Błąd: nawigacja przed zakończeniem inicjalizacji

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.

Błąd: ponowna nawigacja przy powrocie do ekranu

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.

Best Practice: jeden NavController lub Coordinator

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.

Best Practice: testowanie scenariuszy odłożonej nawigacji

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

Czym różni się Deferred Navigation od Deep Link?

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.

Czy można łączyć deferred i direct navigation?

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.

Jak uniknąć race condition przy deferred navigation?

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.

Czy Jetpack Compose wspiera deferred navigation?

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.

Jak obsłużyć deferred navigation przy zminimalizowanej aplikacji?

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

  • Deferred Navigation — wzorzec, w którym przejście jest wykonywane po zakończeniu operacji asynchronicznej
  • Główne scenariusze: autoryzacja, Deeplink, powiadomienia push, feature flagi
  • StateFlow/SharedFlow w Android i Combine/async-await w iOS — narzędzia implementacji
  • Deferred Navigation w przeciwieństwie do bezpośredniej nie powoduje stanu wyścigu przy ładowaniu asynchronicznym
  • Navigation Component w Android i Coordinator Pattern w iOS — podstawowe architektury
  • Jeden NavController/Coordinator jest obowiązkowy dla spójnej nawigacji
  • Testowanie deferred navigation wymaga TestDispatcher i mocków dla usług asynchronicznych

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.

Omów projekt

Przeczytaj również