Deferred Navigation is een patroon van uitgestelde navigatie waarbij de overgang naar het volgende scherm plaatsvindt na het voltooien van een asynchrone bewerking, niet direct op het moment van de gebruikersactie. Volgens Android Developers (2024) helpt deferred navigation om race conditions tussen navigatie en gegevens laden te voorkomen, en vereenvoudigt het de afhandeling van overgangen vanuit pushmeldingen en Deeplink. Het belangrijkste verschil — de route wordt berekend nadat alle benodigde gegevens beschikbaar zijn.
Belangrijkste punten
Deferred Navigation is een architectuurpatroon waarbij de navigatiebeslissing wordt uitgesteld tot het moment dat alle benodigde gegevens beschikbaar zijn. In tegenstelling tot een directe overgang, waarbij de gebruiker op een knop drukt en meteen op een nieuw scherm komt, scheidt deferred navigation de triggergebeurtenis en de daadwerkelijke overgang door er een asynchrone bewerking tussen te plaatsen.
Architectonisch is Deferred Navigation gebaseerd op toestandsverandering: het indrukken van een knop start een asynchroon proces, en het abonneren op het resultaat activeert de navigatie. Dit is vooral belangrijk in apps met MVVM- of MVI-architectuur, waarbij ViewModel de toestand beheert en View (Activity, Fragment, SwiftUI View) zich abonneert op wijzigingen en reageert met een overgang. Deze aanpak elimineert de directe afhankelijkheid tussen UI en navigatielogica.
Volgens Google I/O 2023 wordt deferred navigation aanbevolen voor alle scenario's waarin navigatie afhankelijk is van het resultaat van een netwerkverzoek, autorisatiecontrole, het laden van configuratie of machtigingen. Het patroon is ook verplicht bij het verwerken van Deeplink, wanneer de app eerst moet opstarten, het hoofdscherm moet laden en pas daarna naar de doelroute moet gaan.
Deferred Navigation wordt toegepast in scenario's waar een directe overgang leidt tot een onjuiste schermtoestand of laadfouten. Laten we vier hoofdgevallen bekijken waarin deferred navigation verplicht is.
Als een gebruiker op beveiligde inhoud klikt, moet de app eerst het toegangstoken controleren. Directe navigatie naar het inhoudsscherm leidt tot een leeg scherm of fout 401 als het token is verlopen. Deferred navigation controleert het token en gaat alleen bij succes naar het doelscherm. Bij mislukking wordt omgeleid naar het inlogscherm.
Wanneer de app wordt geopend via een externe link, moet eerst het hoofdscherm worden geladen, de navigatiestatus worden hersteld en pas daarna de overgang via Deeplink worden uitgevoerd. Directe navigatie naar het doelscherm zonder hoofdcontext leidt tot anomalieën: een lege navigatiestack of een beschadigde back stack.
Bij het tikken op een pushmelding kan de app zich in drie toestanden bevinden: gesloten, op de achtergrond of actief. Deferred Navigation bepaalt de toestand van de app, laadt de benodigde inhoud en toont pas daarna het doelscherm. iOS kan dit scenario verwerken via UNNotificationContentExtension.
Als de functionaliteit van een scherm wordt beheerd door een feature flag van de server, kan deferred navigation eerst de configuratie opvragen en dan pas het scherm tonen. Als de functie is uitgeschakeld, ziet de gebruiker alternatieve inhoud of een placeholder in plaats van een leeg scherm.
| Scenario | Directe navigatie | Deferred Navigation |
|---|---|---|
| Autorisatie | Leeg scherm bij verlopen token | Doorverwijzing naar inloggen |
| Deeplink | Beschadigde back stack | Correcte navigatiestack |
| Push | Laden zonder context | Gegevens klaar voor overgang |
| Feature flag | Niet-beschikbare functie tonen | Placeholder of alternatief |
Directe navigatie (direct navigation) is een traditionele aanpak waarbij de overgang onmiddellijk wordt uitgevoerd als reactie op een gebeurtenis. De gebruiker drukt op een knop en de UI-router schakelt meteen van scherm. Deze aanpak is eenvoudig en voorspelbaar, maar beperkt in scenario's waar gegevens van de server of voorwaardelijke controles nodig zijn.
Deferred Navigation voegt een laag toe in de vorm van een asynchrone toestand. De gebruikersgebeurtenis start een bewerking en het abonneren op het resultaat beheert de navigatie. Dit verhoogt de complexiteit van de code, maar biedt flexibiliteit: dezelfde trigger kan naar verschillende schermen leiden, afhankelijk van de geladen gegevens.
De keuze tussen twee benaderingen hangt af van de vereisten: als voor het weergeven van een scherm geen asynchrone gegevens nodig zijn, gebruik dan directe navigatie. Als het scherm afhankelijk is van het resultaat van een verzoek, autorisatie of externe omstandigheden, is deferred navigation verplicht. Een hybride aanpak, waarbij een deel van de overgangen direct is en een deel uitgesteld, is de meest voorkomende praktijk in productie-apps.
Android Jetpack biedt mechanismen voor het implementeren van Deferred Navigation op architectuurniveau. Het hoofdidee is dat ViewModel de toestand beheert en Activity of Fragment zich abonneert op wijzigingen en navigatie activeert via NavController.
StateFlow in Kotlin coroutines is het ideale hulpmiddel voor deferred navigation. ViewModel werkt StateFlow bij met de navigatiegebeurtenis en Activity observeert deze en voert de overgang uit. Nadat de gebeurtenis is verwerkt, wordt StateFlow gewist, waardoor herhaalde navigatie wordt voorkomen.
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))
}
}
}
In Activity activeert het abonneren op navigatie NavController met de route uit ViewModel. Om herhaalde navigatie bij schermrotatie te voorkomen, wordt een NavigationEventWrapper gebruikt die de gebeurtenis slechts één keer verwerkt. Jetpack Navigation 2.7+ ondersteunt Safe Args voor typeveilige argumentoverdracht.
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 heeft geen ingebouwde Navigation Component zoals Android Jetpack, dus ontwikkelaars implementeren Deferred Navigation via Coordinator Pattern in combinatie met Combine of async/await. Coordinator beheert de schermstack en neemt navigatiebeslissingen op basis van geladen gegevens.
Coordinator is een object dat de navigatie tussen ViewControllers beheert. In combinatie met Combine publiceert ViewModel gebeurtenissen via PassthroughSubject en abonneert Coordinator zich erop en voert de overgang uit. Deze aanpak scheidt UI volledig van navigatielogica en voldoet aan de aanbevelingen van Apple voor app-architectuur.
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 introduceerde structured concurrency, waarmee uitgestelde navigatie via async/await zonder Combine kan worden geïmplementeerd. ViewModel biedt een async-functie die de route retourneert na het laden van gegevens. Coordinator roept deze functie aan in Task en voert de overgang uit volgens de ontvangen route.
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 vereenvoudigt de verwerking van asynchrone scenario's, maar vereist een gedisciplineerde aanpak van toestandsbeheer. Laten we de belangrijkste fouten bekijken die ontwikkelaars maken bij het implementeren van deferred navigation.
De meest voorkomende fout is het proberen uitvoeren van deferred navigation voordat het hoofdscherm volledig is geïnitialiseerd en NavController of Coordinator klaar is voor de overgang. In Android leidt dit tot IllegalStateException, in iOS tot een ongedefinieerde UI-toestand. Oplossing: zorg ervoor dat de levenscyclus van de component in de toestand STARTED of RESUMED is voordat navigatie wordt gestart.
Als StateFlow of Subject de gebeurtenis niet wist na verwerking, kan de gebruiker bij terugkeer naar het vorige scherm automatisch opnieuw naar hetzelfde scherm worden geleid. Gebruik SharedFlow met replay=0 op Android of CurrentValueSubject met nil na verwerking op iOS, zodat de navigatiegebeurtenis slechts één keer wordt geactiveerd.
Gebruik één centrale component voor alle navigatie in de app. Wanneer elke Activity, Fragment of ViewController zijn eigen navigatiecontroller heeft, wordt deferred navigation tussen verschillende delen van de app chaotisch. Eén Coordinator vergemakkelijkt het debuggen en testen van navigatiescenario's.
Deferred Navigation is moeilijker te testen dan directe navigatie omdat asynchrone bewerkingen een tijdfactor introduceren. Gebruik TestDispatcher in Android (kotlinx-coroutines-test) en XCTestExpectation in iOS voor het simuleren van gegevens laden en controleer of navigatie volgens de verwachte route wordt uitgevoerd. Mock autorisatie- en deeplinkdiensten voor geïsoleerd testen van elk scenario.
Veelgestelde vragen
Deferred Navigation is een patroon voor uitgestelde overgangen dat in elk asynchroon scenario kan worden toegepast. Deep Link is een van de triggers voor deferred navigation, maar niet de enige. Autorisatie en feature flags gebruiken ook uitgestelde navigatie.
Ja, de meeste apps gebruiken een hybride aanpak. Een productlijstscherm (zonder asynchrone afhankelijkheden) kan directe navigatie gebruiken, terwijl een detailscherm met gegevens laden deferred gebruikt. De verdeling wordt bepaald door de architectuur van het specifieke scherm.
Gebruik SharedFlow zonder replay op Android en combineLatest zonder buffering op iOS. Annuleer eerdere abonnementen bij een nieuwe trigger. Dit garandeert dat alleen de laatste navigatiegebeurtenis wordt verwerkt.
Ja, in Compose wordt deferred navigation geïmplementeerd via abonneren op de StateFlow van ViewModel en het aanroepen van NavController.navigate in LaunchedEffect. Google adviseert Navigation Compose met een gebeurtenismodel voor deferred scenario's.
Stel de navigatie uit tot de app weer actief wordt. In Android gebruik je Lifecycle.State.STARTED om gebeurtenissen te filteren. In iOS controleer je UIApplication.State in het Combine- of async/await-blok.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook