Deferred Navigation é um padrão de navegação adiada onde a transição para a próxima tela ocorre após a conclusão de uma operação assíncrona, em vez de diretamente no momento da ação do usuário. De acordo com Android Developers (2024), a navegação adiada ajuda a evitar condições de corrida entre navegação e carregamento de dados, e simplifica o tratamento de transições de notificações push e Deeplinks. A diferença chave é que a rota é calculada depois que todos os dados necessários estão disponíveis.
Pontos Principais
Deferred Navigation é um padrão arquitetural onde a decisão de navegação é adiada até que todos os dados necessários estejam disponíveis. Ao contrário de uma transição direta onde o usuário pressiona um botão e imediatamente chega a uma nova tela, a navegação adiada separa o evento de gatilho e a transição real colocando uma operação assíncrona entre eles.
Arquiteturalmente, Deferred Navigation é construída sobre mudança de estado: o pressionar de um botão inicia um processo assíncrono, e uma assinatura do seu resultado desencadeia a navegação. Isto é especialmente importante em aplicações com arquitetura MVVM ou MVI, onde a ViewModel gerencia o estado e a View (Activity, Fragment, SwiftUI View) assina as mudanças e reage com uma transição. Esta abordagem elimina a dependência direta entre a UI e a lógica de navegação.
De acordo com o Google I/O 2023, a navegação adiada é recomendada para todos os cenários onde a navegação depende do resultado de uma requisição de rede, verificação de autenticação, carregamento de configuração ou permissões. O padrão também é obrigatório ao lidar com Deeplinks, onde a aplicação deve primeiro iniciar, carregar a tela raiz e só então navegar para a rota de destino.
Deferred Navigation é usada em cenários onde a navegação direta leva a um estado de tela incorreto ou erros de carregamento. Vamos examinar quatro casos principais onde a navegação adiada é necessária.
Se um usuário toca em conteúdo protegido, a aplicação deve primeiro verificar o token de acesso. A navegação direta para a tela de conteúdo resultará em uma tela vazia ou erro 401 se o token expirou. A navegação adiada verifica o token, e apenas em caso de sucesso — navega para a tela de destino. Em caso de falha — redireciona para a tela de login.
Quando uma aplicação é aberta através de um link externo, primeiro deve carregar a tela raiz, restaurar o estado de navegação e só então realizar a transição Deeplink. A navegação direta para a tela de destino sem contexto raiz levará a anomalias: uma pilha de navegação vazia ou uma pilha de retorno quebrada.
Ao tocar em uma notificação push, a aplicação pode estar em um de três estados: fechada, em segundo plano ou ativa. Deferred Navigation determina o estado da aplicação, carrega o conteúdo necessário e só então exibe a tela de destino. O iOS permite lidar com este cenário através de UNNotificationContentExtension.
Se a funcionalidade de uma tela é controlada por um feature flag do servidor, a navegação adiada permite primeiro solicitar a configuração e só então mostrar a tela. Se a funcionalidade estiver desabilitada — o usuário vê conteúdo alternativo ou um placeholder em vez de uma tela vazia.
| Cenário | Navegação direta | Deferred Navigation |
|---|---|---|
| Autorização | Tela vazia com token expirado | Redirecionamento para login |
| Deeplink | Pilha de retorno quebrada | Pilha de navegação correta |
| Push | Carregamento sem contexto | Dados prontos antes da transição |
| Feature flag | Exibir funcionalidade indisponível | Placeholder ou alternativa |
A navegação direta é uma abordagem tradicional onde a transição é realizada imediatamente em resposta a um evento. O usuário pressiona um botão, e o roteador de UI muda a tela imediatamente. Esta abordagem é simples e previsível, mas limitada em cenários que exigem dados do servidor ou verificação de condições.
Deferred Navigation adiciona uma camada na forma de estado assíncrono. Um evento do usuário inicia uma operação, e uma assinatura do resultado controla a navegação. Isto aumenta a complexidade do código, mas oferece flexibilidade: o mesmo gatilho pode levar a diferentes telas dependendo dos dados carregados.
A escolha entre as duas abordagens depende dos requisitos: se exibir uma tela não requer dados assíncronos — use navegação direta. Se a tela depende do resultado de uma requisição, autorização ou condições externas — a navegação adiada é necessária. Uma abordagem híbrida, onde algumas transições são diretas e outras adiadas, é a prática mais comum em aplicações industriais.
Android Jetpack fornece mecanismos para implementar Deferred Navigation no nível da arquitetura. A ideia principal é que a ViewModel gerencia o estado, enquanto a Activity ou Fragment assina as mudanças e desencadeia a navegação através do NavController.
StateFlow em corrotinas Kotlin é a ferramenta ideal para navegação adiada. A ViewModel atualiza um StateFlow com um evento de navegação, e a Activity o observa e realiza a transição. Uma vez que o evento é processado, o StateFlow é limpo, evitando navegação 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))
}
}
}
Na Activity, uma assinatura de navegação aciona o NavController com a rota da ViewModel. Para evitar navegação repetida ao rotacionar a tela, é usado um wrapper NavigationEventWrapper que processa o evento apenas uma vez. Jetpack Navigation 2.7+ suporta Safe Args para passagem de argumentos 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ão tem um Navigation Component embutido similar ao Android Jetpack, então os desenvolvedores implementam Deferred Navigation através do Coordinator Pattern em combinação com Combine ou async/await. O Coordinator gerencia a pilha de telas e toma decisões de navegação com base nos dados carregados.
Coordinator é um objeto que gerencia a navegação entre ViewControllers. Em combinação com Combine, a ViewModel publica eventos através de PassthroughSubject, e o Coordinator os assina e realiza a transição. Esta abordagem separa completamente a UI da lógica de navegação e segue as recomendações da Apple para arquitetura de aplicações.
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 introduziu concorrência estruturada, que permite implementar navegação adiada através de async/await sem Combine. A ViewModel fornece uma função async que retorna uma rota após carregar os dados. O Coordinator chama esta função em uma Task e realiza a transição com base na rota recebida.
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 o tratamento de cenários assíncronos, mas requer uma abordagem disciplinada ao gerenciamento de estado. Vamos examinar os principais erros que os desenvolvedores cometem ao implementar navegação adiada.
O erro mais comum é tentar realizar navegação adiada antes que a tela raiz esteja completamente inicializada e o NavController ou Coordinator esteja pronto para a transição. No Android isto leva a IllegalStateException, no iOS — a um estado de UI indefinido. A solução é garantir que o ciclo de vida do componente esteja no estado STARTED ou RESUMED antes de desencadear a navegação.
Se o StateFlow ou Subject não limpar o evento após o processamento, ao retornar à tela anterior o usuário pode ser automaticamente redirecionado para a mesma tela novamente. Use SharedFlow com replay=0 no Android ou CurrentValueSubject com nil após o processamento no iOS, para que o evento de navegação dispare apenas uma vez.
Use um componente central para toda a navegação na aplicação. Quando cada Activity, Fragment ou ViewController tem seu próprio controlador de navegação, a navegação adiada entre diferentes partes da aplicação se torna caótica. Um único Coordinator simplifica a depuração e o teste de cenários de navegação.
Deferred Navigation é mais difícil de testar do que a navegação direta porque operações assíncronas introduzem um fator tempo. Use TestDispatcher no Android (kotlinx-coroutines-test) e XCTestExpectation no iOS para simular carregamento de dados e verificar que a navegação segue a rota esperada. Simule serviços de autorização e deeplinks para teste isolado de cada cenário.
Perguntas Frequentes
Deferred Navigation é um padrão de transição adiada que pode ser aplicado em qualquer cenário assíncrono. Deep Link é um gatilho para navegação adiada, mas não o único. Autorização e feature flags também usam navegação adiada.
Sim, a maioria das aplicações usa uma abordagem híbrida. Uma tela de lista de produtos (sem dependências assíncronas) pode usar navegação direta, enquanto uma tela de detalhes com carregamento de dados usa adiada. A separação é determinada pela arquitetura de cada tela específica.
Use SharedFlow sem replay no Android e combineLatest sem buffer no iOS. Cancele assinaturas anteriores em um novo gatilho. Isto garante que apenas o último evento de navegação seja processado.
Sim, no Compose a navegação adiada é implementada através de assinatura do StateFlow da ViewModel e chamada de NavController.navigate em LaunchedEffect. O Google recomenda usar Navigation Compose com um modelo orientado a eventos para cenários adiados.
Adie a navegação até que o aplicativo se torne ativo novamente. No Android use Lifecycle.State.STARTED para filtrar eventos. No iOS verifique UIApplication.State em blocos Combine ou async/await.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também