Deferred Navigation est un modèle de navigation différée où la transition vers l'écran suivant se produit après l'achèvement d'une opération asynchrone, plutôt que directement au moment de l'action de l'utilisateur. Selon Android Developers (2024), la navigation différée permet d'éviter les conditions de course entre la navigation et le chargement des données, et simplifie le traitement des transitions depuis les notifications push et les Deeplinks. La différence clé — l'itinéraire est calculé après que toutes les données nécessaires sont disponibles.
Points clés
Deferred Navigation est un modèle architectural où la décision de navigation est reportée jusqu'à ce que toutes les données nécessaires soient disponibles. Contrairement à une transition directe où l'utilisateur appuie sur un bouton et arrive immédiatement sur un nouvel écran, la navigation différée sépare l'événement déclencheur et la transition réelle en plaçant une opération asynchrone entre eux.
Architecturalement, Deferred Navigation repose sur le changement d'état : l'appui sur un bouton initie un processus asynchrone, et un abonnement à son résultat déclenche la navigation. Ceci est particulièrement important dans les applications avec une architecture MVVM ou MVI, où la ViewModel gère l'état et la View (Activity, Fragment, SwiftUI View) s'abonne aux changements et réagit par une transition. Cette approche élimine la dépendance directe entre l'UI et la logique de navigation.
Selon Google I/O 2023, la navigation différée est recommandée pour tous les scénarios où la navigation dépend du résultat d'une requête réseau, d'une vérification d'authentification, d'un chargement de configuration ou d'autorisations. Le modèle est également obligatoire lors du traitement des Deeplinks, où l'application doit d'abord lancer, charger l'écran racine et seulement ensuite naviguer vers la route cible.
Deferred Navigation est utilisée dans les scénarios où la navigation directe conduit à un état d'écran incorrect ou à des erreurs de chargement. Examinons quatre cas principaux où la navigation différée est nécessaire.
Si un utilisateur tape sur un contenu protégé, l'application doit d'abord vérifier le jeton d'accès. La navigation directe vers l'écran de contenu entraînera un écran vide ou une erreur 401 si le jeton a expiré. La navigation différée vérifie le jeton, et seulement en cas de succès — navigue vers l'écran cible. En cas d'échec — redirige vers l'écran de connexion.
Lorsqu'une application est ouverte via un lien externe, elle doit d'abord charger l'écran racine, restaurer l'état de navigation et seulement ensuite effectuer la transition Deeplink. La navigation directe vers l'écran cible sans contexte racine entraînera des anomalies : une pile de navigation vide ou une pile arrière brisée.
En tapant sur une notification push, l'application peut être dans l'un des trois états : fermée, en arrière-plan ou active. Deferred Navigation détermine l'état de l'application, charge le contenu nécessaire et seulement ensuite affiche l'écran cible. iOS permet de traiter ce scénario via UNNotificationContentExtension.
Si la fonctionnalité d'un écran est contrôlée par un feature flag du serveur, la navigation différée permet d'abord de demander la configuration et seulement ensuite d'afficher l'écran. Si la fonction est désactivée — l'utilisateur voit un contenu alternatif ou un espace réservé au lieu d'un écran vide.
| Scénario | Navigation directe | Deferred Navigation |
|---|---|---|
| Autorisation | Écran vide avec jeton expiré | Redirection vers la connexion |
| Deeplink | Pile arrière brisée | Pile de navigation correcte |
| Push | Chargement sans contexte | Données prêtes avant la transition |
| Feature flag | Affichage de fonctionnalité indisponible | Espace réservé ou alternative |
La navigation directe est une approche traditionnelle où la transition est effectuée immédiatement en réponse à un événement. L'utilisateur appuie sur un bouton, et le routeur d'UI change immédiatement d'écran. Cette approche est simple et prévisible mais limitée dans les scénarios qui nécessitent des données du serveur ou une vérification de conditions.
Deferred Navigation ajoute une couche sous forme d'état asynchrone. Un événement utilisateur lance une opération, et un abonnement au résultat contrôle la navigation. Cela augmente la complexité du code mais offre de la flexibilité : le même déclencheur peut mener à différents écrans selon les données chargées.
Le choix entre les deux approches dépend des exigences : si l'affichage d'un écran ne nécessite pas de données asynchrones — utilisez la navigation directe. Si l'écran dépend du résultat d'une requête, d'une autorisation ou de conditions externes — la navigation différée est nécessaire. Une approche hybride, où certaines transitions sont directes et d'autres différées, est la pratique la plus courante dans les applications industrielles.
Android Jetpack fournit des mécanismes pour implémenter Deferred Navigation au niveau de l'architecture. L'idée principale est que la ViewModel gère l'état, tandis que l'Activity ou le Fragment s'abonne aux changements et déclenche la navigation via NavController.
StateFlow dans les coroutines Kotlin est l'outil idéal pour la navigation différée. La ViewModel met à jour un StateFlow avec un événement de navigation, et l'Activity l'observe et effectue la transition. Une fois l'événement traité, le StateFlow est nettoyé, empêchant la navigation répétée.
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))
}
}
}
Dans l'Activity, un abonnement à la navigation déclenche NavController avec l'itinéraire depuis la ViewModel. Pour éviter la navigation répétée lors de la rotation de l'écran, un wrapper NavigationEventWrapper est utilisé, qui ne traite l'événement qu'une seule fois. Jetpack Navigation 2.7+ prend en charge Safe Args pour le passage d'arguments 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 n'a pas de Navigation Component intégré similaire à Android Jetpack, donc les développeurs implémentent Deferred Navigation via le Coordinator Pattern en combinaison avec Combine ou async/await. Le Coordinator gère la pile d'écrans et prend des décisions de navigation basées sur les données chargées.
Coordinator est un objet qui gère la navigation entre les ViewControllers. En combinaison avec Combine, la ViewModel publie des événements via PassthroughSubject, et le Coordinator s'y abonne et effectue la transition. Cette approche sépare complètement l'UI de la logique de navigation et suit les recommandations d'Apple pour l'architecture des applications.
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 a introduit la concurrence structurée, qui permet d'implémenter la navigation différée via async/await sans Combine. La ViewModel fournit une fonction async qui retourne un itinéraire après le chargement des données. Le Coordinator appelle cette fonction dans une Task et effectue la transition basée sur l'itinéraire reçu.
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 simplifie le traitement des scénarios asynchrones mais nécessite une approche disciplinée de la gestion d'état. Examinons les principales erreurs que les développeurs commettent lors de l'implémentation de la navigation différée.
L'erreur la plus courante est d'essayer d'effectuer une navigation différée avant que l'écran racine soit complètement initialisé et que le NavController ou Coordinator soit prêt pour la transition. Sur Android cela conduit à IllegalStateException, sur iOS — à un état d'UI indéfini. La solution est de s'assurer que le cycle de vie du composant est dans l'état STARTED ou RESUMED avant de déclencher la navigation.
Si le StateFlow ou Subject ne nettoie pas l'événement après le traitement, lors du retour à l'écran précédent l'utilisateur peut être automatiquement redirigé vers le même écran. Utilisez SharedFlow avec replay=0 sur Android ou CurrentValueSubject avec nil après le traitement sur iOS, afin que l'événement de navigation ne se déclenche qu'une seule fois.
Utilisez un composant central pour toute la navigation dans l'application. Lorsque chaque Activity, Fragment ou ViewController a son propre contrôleur de navigation, la navigation différée entre différentes parties de l'application devient chaotique. Un seul Coordinator simplifie le débogage et les tests des scénarios de navigation.
Deferred Navigation est plus difficile à tester que la navigation directe car les opérations asynchrones introduisent un facteur temps. Utilisez TestDispatcher sur Android (kotlinx-coroutines-test) et XCTestExpectation sur iOS pour simuler le chargement des données et vérifier que la navigation suit l'itinéraire attendu. Simulez les services d'autorisation et de deeplinks pour des tests isolés de chaque scénario.
Foire aux questions
Deferred Navigation est un modèle de transition différée qui peut être appliqué dans tout scénario asynchrone. Deep Link est un déclencheur pour la navigation différée, mais pas le seul. L'authentification et les feature flags utilisent également la navigation différée.
Oui, la plupart des applications utilisent une approche hybride. Un écran de liste de produits (sans dépendances asynchrones) peut utiliser la navigation directe, tandis qu'un écran de détails avec chargement de données utilise la différée. La séparation est déterminée par l'architecture de chaque écran spécifique.
Utilisez SharedFlow sans replay sur Android et combineLatest sans mise en mémoire tampon sur iOS. Annulez les abonnements précédents lors d'un nouveau déclencheur. Cela garantit que seul le dernier événement de navigation est traité.
Oui, dans Compose la navigation différée est implémentée via l'abonnement au StateFlow de la ViewModel et l'appel de NavController.navigate dans LaunchedEffect. Google recommande d'utiliser Navigation Compose avec un modèle événementiel pour les scénarios différés.
Reportez la navigation jusqu'à ce que l'application redevienne active. Sur Android utilisez Lifecycle.State.STARTED pour filtrer les événements. Sur iOS vérifiez UIApplication.State dans les blocs Combine ou async/await.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi