Deferred Navigation es un patrón de navegación diferida donde la transición a la siguiente pantalla ocurre después de que se completa una operación asíncrona, en lugar de directamente en el momento de la acción del usuario. Según Android Developers (2024), la navegación diferida ayuda a evitar condiciones de carrera entre la navegación y la carga de datos, y simplifica el manejo de transiciones desde notificaciones push y Deeplinks. La diferencia clave es que la ruta se calcula después de que todos los datos necesarios estén disponibles.
Puntos Clave
Deferred Navigation es un patrón arquitectónico donde la decisión de navegación se pospone hasta que todos los datos necesarios estén disponibles. A diferencia de una transición directa donde el usuario presiona un botón y llega inmediatamente a una nueva pantalla, la navegación diferida separa el evento desencadenante y la transición real colocando una operación asíncrona entre ellos.
Arquitectónicamente, Deferred Navigation se basa en el cambio de estado: la pulsación de un botón inicia un proceso asíncrono, y una suscripción a su resultado desencadena la navegación. Esto es especialmente importante en aplicaciones con arquitectura MVVM o MVI, donde la ViewModel gestiona el estado y la Vista (Activity, Fragment, SwiftUI View) se suscribe a los cambios y reacciona con una transición. Este enfoque elimina la dependencia directa entre la UI y la lógica de navegación.
Según Google I/O 2023, la navegación diferida se recomienda para todos los escenarios donde la navegación depende del resultado de una solicitud de red, verificación de autenticación, carga de configuración o permisos. El patrón también es obligatorio al manejar Deeplinks, donde la aplicación debe primero iniciarse, cargar la pantalla raíz y solo entonces navegar a la ruta de destino.
Deferred Navigation se utiliza en escenarios donde la navegación directa conduce a un estado de pantalla incorrecto o errores de carga. Examinemos cuatro casos principales donde la navegación diferida es necesaria.
Si un usuario toca contenido protegido, la aplicación debe primero verificar el token de acceso. La navegación directa a la pantalla de contenido resultará en una pantalla vacía o un error 401 si el token ha expirado. La navegación diferida verifica el token, y solo en caso de éxito — navega a la pantalla de destino. En caso de fallo — redirige a la pantalla de inicio de sesión.
Cuando una aplicación se abre mediante un enlace externo, primero debe cargar la pantalla raíz, restaurar el estado de navegación y solo entonces realizar la transición Deeplink. La navegación directa a la pantalla de destino sin contexto raíz provocará anomalías: una pila de navegación vacía o una pila atrás rota.
Al tocar una notificación push, la aplicación puede estar en uno de tres estados: cerrada, en segundo plano o activa. Deferred Navigation determina el estado de la aplicación, carga el contenido necesario y solo entonces muestra la pantalla de destino. iOS permite manejar este escenario a través de UNNotificationContentExtension.
Si la funcionalidad de una pantalla está controlada por un feature flag del servidor, la navegación diferida permite primero solicitar la configuración y solo entonces mostrar la pantalla. Si la función está deshabilitada — el usuario ve contenido alternativo o un marcador de posición en lugar de una pantalla vacía.
| Escenario | Navegación directa | Deferred Navigation |
|---|---|---|
| Autorización | Pantalla vacía con token expirado | Redirección al inicio de sesión |
| Deeplink | Pila atrás rota | Pila de navegación correcta |
| Push | Carga sin contexto | Datos listos antes de la transición |
| Feature flag | Mostrar funcionalidad no disponible | Marcador de posición o alternativa |
La navegación directa es un enfoque tradicional donde la transición se realiza inmediatamente en respuesta a un evento. El usuario presiona un botón y el enrutador de UI cambia la pantalla de inmediato. Este enfoque es simple y predecible, pero limitado en escenarios que requieren datos del servidor o verificación de condiciones.
Deferred Navigation añade una capa en forma de estado asíncrono. Un evento de usuario inicia una operación, y una suscripción al resultado controla la navegación. Esto aumenta la complejidad del código pero proporciona flexibilidad: el mismo desencadenante puede llevar a diferentes pantallas dependiendo de los datos cargados.
La elección entre los dos enfoques depende de los requisitos: si mostrar una pantalla no requiere datos asíncronos — use navegación directa. Si la pantalla depende del resultado de una solicitud, autorización o condiciones externas — la navegación diferida es necesaria. Un enfoque híbrido, donde algunas transiciones son directas y otras diferidas, es la práctica más común en aplicaciones industriales.
Android Jetpack proporciona mecanismos para implementar Deferred Navigation a nivel de arquitectura. La idea principal es que la ViewModel gestiona el estado, mientras que la Activity o Fragment se suscribe a los cambios y desencadena la navegación a través de NavController.
StateFlow en corrutinas de Kotlin es la herramienta ideal para la navegación diferida. La ViewModel actualiza un StateFlow con un evento de navegación, y la Activity lo observa y realiza la transición. Una vez procesado el evento, el StateFlow se limpia, evitando la navegación repetida.
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))
}
}
}
En la Activity, una suscripción a la navegación activa NavController con la ruta desde la ViewModel. Para evitar la navegación repetida al rotar la pantalla, se utiliza un wrapper NavigationEventWrapper que procesa el evento solo una vez. Jetpack Navigation 2.7+ soporta Safe Args para el paso de argumentos con seguridad de tipos.
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 no tiene un Navigation Component integrado similar a Android Jetpack, por lo que los desarrolladores implementan Deferred Navigation a través del Coordinator Pattern en combinación con Combine o async/await. El Coordinator gestiona la pila de pantallas y toma decisiones de navegación basadas en los datos cargados.
Coordinator es un objeto que gestiona la navegación entre ViewControllers. En combinación con Combine, la ViewModel publica eventos a través de PassthroughSubject, y el Coordinator se suscribe a ellos y realiza la transición. Este enfoque separa completamente la UI de la lógica de navegación y sigue las recomendaciones de Apple para la arquitectura de aplicaciones.
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 introdujo la concurrencia estructurada, que permite implementar la navegación diferida mediante async/await sin Combine. La ViewModel proporciona una función async que devuelve una ruta después de cargar los datos. El Coordinator llama a esta función en un Task y realiza la transición basándose en la ruta recibida.
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 simplifica el manejo de escenarios asíncronos pero requiere un enfoque disciplinado en la gestión del estado. Examinemos los principales errores que cometen los desarrolladores al implementar la navegación diferida.
El error más común es intentar realizar navegación diferida antes de que la pantalla raíz esté completamente inicializada y el NavController o Coordinator esté listo para la transición. En Android esto lleva a IllegalStateException, en iOS — a un estado de UI indefinido. La solución es asegurarse de que el ciclo de vida del componente esté en estado STARTED o RESUMED antes de desencadenar la navegación.
Si el StateFlow o Subject no limpia el evento después del procesamiento, al regresar a la pantalla anterior el usuario puede ser redirigido automáticamente a la misma pantalla nuevamente. Use SharedFlow con replay=0 en Android o CurrentValueSubject con nil después del procesamiento en iOS, para que el evento de navegación se active solo una vez.
Use un componente central para toda la navegación en la aplicación. Cuando cada Activity, Fragment o ViewController tiene su propio controlador de navegación, la navegación diferida entre diferentes partes de la aplicación se vuelve caótica. Un único Coordinator simplifica la depuración y las pruebas de escenarios de navegación.
Deferred Navigation es más difícil de probar que la navegación directa porque las operaciones asíncronas introducen un factor de tiempo. Use TestDispatcher en Android (kotlinx-coroutines-test) y XCTestExpectation en iOS para simular la carga de datos y verificar que la navegación sigue la ruta esperada. Simule servicios de autorización y deeplinks para pruebas aisladas de cada escenario.
Preguntas Frecuentes
Deferred Navigation es un patrón de transición diferida que se puede aplicar en cualquier escenario asíncrono. Deep Link es un desencadenante para la navegación diferida, pero no el único. La autorización y los feature flags también usan navegación diferida.
Sí, la mayoría de las aplicaciones usan un enfoque híbrido. Una pantalla de lista de productos (sin dependencias asíncronas) puede usar navegación directa, mientras que una pantalla de detalles con carga de datos usa diferida. La separación se determina por la arquitectura de cada pantalla específica.
Use SharedFlow sin replay en Android y combineLatest sin almacenamiento en búfer en iOS. Cancele suscripciones anteriores ante un nuevo desencadenante. Esto garantiza que solo se procese el último evento de navegación.
Sí, en Compose la navegación diferida se implementa mediante suscripción al StateFlow de la ViewModel y llamada a NavController.navigate en LaunchedEffect. Google recomienda usar Navigation Compose con un modelo basado en eventos para escenarios diferidos.
Retrase la navegación hasta que la aplicación vuelva a estar activa. En Android use Lifecycle.State.STARTED para filtrar eventos. En iOS verifique UIApplication.State en bloques Combine o async/await.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también