Deferred Navigation è un pattern di navigazione differita in cui la transizione allo schermo successivo avviene dopo il completamento di un'operazione asincrona, anziché direttamente nel momento dell'azione dell'utente. Secondo Android Developers (2024), la navigazione differita aiuta a evitare race condition tra navigazione e caricamento dati, e semplifica la gestione delle transizioni da notifiche push e Deeplink. La differenza chiave — il percorso viene calcolato dopo che tutti i dati necessari sono disponibili.
Punti chiave
Deferred Navigation è un pattern architetturale in cui la decisione di navigazione viene rimandata fino a quando tutti i dati necessari sono disponibili. A differenza di una transizione diretta in cui l'utente preme un pulsante e arriva immediatamente a un nuovo schermo, la navigazione differita separa l'evento trigger e la transizione effettiva inserendo un'operazione asincrona tra di essi.
Architetturalmente, Deferred Navigation si basa sul cambiamento di stato: la pressione di un pulsante avvia un processo asincrono, e una sottoscrizione al suo risultato attiva la navigazione. Questo è particolarmente importante nelle applicazioni con architettura MVVM o MVI, dove la ViewModel gestisce lo stato e la View (Activity, Fragment, SwiftUI View) si sottoscrive ai cambiamenti e reagisce con una transizione. Questo approccio elimina la dipendenza diretta tra UI e logica di navigazione.
Secondo Google I/O 2023, la navigazione differita è raccomandata per tutti gli scenari in cui la navigazione dipende dal risultato di una richiesta di rete, verifica di autenticazione, caricamento di configurazione o permessi. Il pattern è anche obbligatorio nella gestione dei Deeplink, dove l'app deve prima avviarsi, caricare lo schermo principale e solo poi navigare verso il percorso di destinazione.
Deferred Navigation viene utilizzata in scenari in cui la navigazione diretta porta a uno stato dello schermo errato o a errori di caricamento. Esaminiamo quattro casi principali in cui la navigazione differita è necessaria.
Se un utente tocca un contenuto protetto, l'applicazione deve prima verificare il token di accesso. La navigazione diretta allo schermo del contenuto risulterà in uno schermo vuoto o un errore 401 se il token è scaduto. La navigazione differita verifica il token, e solo in caso di successo — naviga allo schermo di destinazione. In caso di fallimento — reindirizza alla schermata di login.
Quando un'applicazione viene aperta tramite un link esterno, deve prima caricare lo schermo principale, ripristinare lo stato di navigazione e solo poi eseguire la transizione Deeplink. La navigazione diretta allo schermo di destinazione senza contesto principale porterà ad anomalie: uno stack di navigazione vuoto o uno stack indietro rotto.
Toccando una notifica push, l'applicazione può trovarsi in uno di tre stati: chiusa, in background o attiva. Deferred Navigation determina lo stato dell'applicazione, carica il contenuto necessario e solo poi mostra lo schermo di destinazione. iOS permette di gestire questo scenario tramite UNNotificationContentExtension.
Se la funzionalità di uno schermo è controllata da un feature flag dal server, la navigazione differita permette di richiedere prima la configurazione e solo poi mostrare lo schermo. Se la funzionalità è disabilitata — l'utente vede contenuto alternativo o un placeholder invece di uno schermo vuoto.
| Scenario | Navigazione diretta | Deferred Navigation |
|---|---|---|
| Autorizzazione | Schermo vuoto con token scaduto | Reindirizzamento al login |
| Deeplink | Stack indietro rotto | Stack di navigazione corretto |
| Push | Caricamento senza contesto | Dati pronti prima della transizione |
| Feature flag | Mostrare funzionalità non disponibile | Placeholder o alternativa |
La navigazione diretta è un approccio tradizionale in cui la transizione viene eseguita immediatamente in risposta a un evento. L'utente preme un pulsante e il router dell'UI cambia immediatamente schermo. Questo approccio è semplice e prevedibile ma limitato in scenari che richiedono dati dal server o verifica di condizioni.
Deferred Navigation aggiunge un livello sotto forma di stato asincrono. Un evento utente avvia un'operazione, e una sottoscrizione al risultato controlla la navigazione. Questo aumenta la complessità del codice ma offre flessibilità: lo stesso trigger può portare a schermi diversi a seconda dei dati caricati.
La scelta tra i due approcci dipende dai requisiti: se mostrare uno schermo non richiede dati asincroni — usa la navigazione diretta. Se lo schermo dipende dal risultato di una richiesta, autorizzazione o condizioni esterne — la navigazione differita è necessaria. Un approccio ibrido, dove alcune transizioni sono dirette e altre differite, è la pratica più comune nelle applicazioni industriali.
Android Jetpack fornisce meccanismi per implementare Deferred Navigation a livello architetturale. L'idea principale è che la ViewModel gestisce lo stato, mentre l'Activity o il Fragment si sottoscrive ai cambiamenti e attiva la navigazione tramite NavController.
StateFlow nelle coroutine Kotlin è lo strumento ideale per la navigazione differita. La ViewModel aggiorna uno StateFlow con un evento di navigazione, e l'Activity lo osserva ed esegue la transizione. Una volta che l'evento è stato elaborato, lo StateFlow viene pulito, prevenendo la navigazione ripetuta.
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))
}
}
}
Nell'Activity, una sottoscrizione alla navigazione attiva NavController con il percorso dalla ViewModel. Per prevenire la navigazione ripetuta durante la rotazione dello schermo, viene utilizzato un wrapper NavigationEventWrapper che elabora l'evento solo una volta. Jetpack Navigation 2.7+ supporta Safe Args per il passaggio di argomenti type-safe.
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 non ha un Navigation Component integrato simile ad Android Jetpack, quindi gli sviluppatori implementano Deferred Navigation tramite il Coordinator Pattern in combinazione con Combine o async/await. Il Coordinator gestisce lo stack degli schermi e prende decisioni di navigazione basate sui dati caricati.
Coordinator è un oggetto che gestisce la navigazione tra ViewController. In combinazione con Combine, la ViewModel pubblica eventi tramite PassthroughSubject, e il Coordinator vi si sottoscrive ed esegue la transizione. Questo approccio separa completamente l'UI dalla logica di navigazione e segue le raccomandazioni di Apple per l'architettura delle applicazioni.
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 ha introdotto la concorrenza strutturata, che permette di implementare la navigazione differita tramite async/await senza Combine. La ViewModel fornisce una funzione async che restituisce un percorso dopo il caricamento dei dati. Il Coordinator chiama questa funzione in un Task ed esegue la transizione basata sul percorso ricevuto.
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 semplifica la gestione degli scenari asincroni ma richiede un approccio disciplinato alla gestione dello stato. Esaminiamo i principali errori che gli sviluppatori commettono nell'implementare la navigazione differita.
L'errore più comune è tentare di eseguire una navigazione differita prima che lo schermo principale sia completamente inizializzato e il NavController o Coordinator sia pronto per la transizione. Su Android questo porta a IllegalStateException, su iOS — a uno stato UI indefinito. La soluzione è assicurarsi che il ciclo di vita del componente sia nello stato STARTED o RESUMED prima di attivare la navigazione.
Se lo StateFlow o Subject non pulisce l'evento dopo l'elaborazione, al ritorno sullo schermo precedente l'utente può essere automaticamente reindirizzato allo stesso schermo. Usa SharedFlow con replay=0 su Android o CurrentValueSubject con nil dopo l'elaborazione su iOS, in modo che l'evento di navigazione si attivi solo una volta.
Usa un componente centrale per tutta la navigazione nell'applicazione. Quando ogni Activity, Fragment o ViewController ha il proprio controller di navigazione, la navigazione differita tra diverse parti dell'applicazione diventa caotica. Un unico Coordinator semplifica il debugging e il test degli scenari di navigazione.
Deferred Navigation è più difficile da testare della navigazione diretta perché le operazioni asincrone introducono un fattore tempo. Usa TestDispatcher su Android (kotlinx-coroutines-test) e XCTestExpectation su iOS per simulare il caricamento dei dati e verificare che la navigazione segua il percorso atteso. Mocka i servizi di autorizzazione e deeplink per test isolati di ogni scenario.
Domande frequenti
Deferred Navigation è un pattern di transizione differita che può essere applicato in qualsiasi scenario asincrono. Deep Link è un trigger per la navigazione differita, ma non l'unico. Anche l'autenticazione e i feature flag utilizzano la navigazione differita.
Sì, la maggior parte delle applicazioni utilizza un approccio ibrido. Uno schermo di elenco prodotti (senza dipendenze asincrone) può usare la navigazione diretta, mentre uno schermo di dettagli con caricamento dati usa quella differita. La separazione è determinata dall'architettura di ogni specifico schermo.
Usa SharedFlow senza replay su Android e combineLatest senza buffer su iOS. Cancella le sottoscrizioni precedenti a un nuovo trigger. Questo garantisce che solo l'ultimo evento di navigazione venga elaborato.
Sì, in Compose la navigazione differita viene implementata tramite sottoscrizione al StateFlow della ViewModel e chiamata di NavController.navigate in LaunchedEffect. Google raccomanda di usare Navigation Compose con un modello event-driven per scenari differiti.
Rimanda la navigazione fino a quando l'applicazione diventa nuovamente attiva. Su Android usa Lifecycle.State.STARTED per filtrare gli eventi. Su iOS verifica UIApplication.State nei blocchi Combine o async/await.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche