Navegação e Roteamento — Visão Geral
Última atualização: 15 de julho de 2026
O que este documento cobre
Este fluxo documenta a estrutura de rotas do app e o layout raiz, a parte que fica por trás de todos os outros fluxos já documentados. Duas peças centrais de navegação já foram cobertas em detalhe em outros lugares, e não são repetidas aqui: o gate de autenticação na entrada do app e a restauração da última rota visitada, ambos em Guardas de Rota e Inicialização, e o padrão de esconder uma tab por flag de configuração enquanto a própria rota também se protege com um redirect, visto em Carteira de Clientes e Catálogo de Produtos.
Estrutura de app/
O Expo Router usa a estrutura de arquivos dentro de app/ como a árvore de rotas. Em alto nível:
| Caminho | O que é |
|---|---|
app/(tabs)/ | O grupo de tabs principal: home, clientes, vendas, catalogo, loja |
app/auth/ | Tela de login, documentada em Login e Callback OAuth |
app/nova-venda.tsx + app/nova-venda/* | O fluxo de nova venda, documentado em Nova Venda |
app/cliente/, app/detalhes.cliente.tsx | Cadastro e detalhes de cliente, documentados em Carteira de Clientes |
app/catalog/ | Análise de produto e catálogo digital, documentados em Catálogo de Produtos |
app/home/, app/loja/ | Dezenas de subtelas de indicadores, chat interno, apontamento de inventário e gestão, ainda não documentadas como fluxo próprio |
app/detalhes-venda.tsx, app/faturamento-venda.tsx, app/pagamento-venda.tsx | Telas de detalhe/faturamento/pagamento de venda, fora do editor de rascunho |
app/index.tsx | Gate de entrada, documentado em Guardas de Rota e Inicialização |
Não existe uma pasta de rotas para sincronização. O fluxo de sincronização, documentado em Sincronização Offline, não é uma tela navegável, é um modal (SyncModal) montado direto no layout raiz, sempre presente, controlado só pelo estado do SyncProvider.
O layout raiz (app/_layout.tsx)
Esse arquivo é o único lugar do app onde toda a árvore de providers é montada, numa ordem específica, de fora para dentro:
<QueryClientProvider client={queryClient}>
<RoutePersistence />
<SafeAreaProvider>
<ToastProvider>
<ThemeProvider value={navigationTheme}>
<SyncProvider>
<GestureHandlerRootView>
<BottomSheetModalProvider>
<AppAlertProvider>
<AuthProvider>
<OrganizationSelectorProvider>
<VirtualAssistantProvider>
<GlobalStatusBar />
<Stack screenOptions={{ headerShown: false, animation: "none" }}>
{/* ... */}
</Stack>
<PermissionModal />
<StatusBar style="dark" />
</VirtualAssistantProvider>
</OrganizationSelectorProvider>
</AuthProvider>
</AppAlertProvider>
</BottomSheetModalProvider>
<SyncModalWrapper />
<NavigationLoadingOverlay />
{showAnimatedSplash && <AnimatedSplash onFinish={handleAnimatedSplashFinish} />}
</GestureHandlerRootView>
</SyncProvider>
</ThemeProvider>
</ToastProvider>
</SafeAreaProvider>
</QueryClientProvider>
Vale notar que SyncModalWrapper e NavigationLoadingOverlay ficam fora da árvore de AuthProvider, então continuam podendo aparecer mesmo em telas fora do contexto autenticado. AppAlertProvider e BottomSheetModalProvider, por outro lado, ficam por dentro, disponíveis para tudo que precisa de alerta ou bottom sheet nas telas autenticadas.
Uma rota registrada no Stack, mas que não existe
O Stack do layout raiz declara explicitamente cinco telas:
<Stack.Screen name="auth" options={{ headerShown: false }} />
<Stack.Screen name="permissions" options={{ headerShown: false }} />
<Stack.Screen name="cliente/registrar-cliente" options={{ headerShown: false }} />
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
Não existe app/permissions.tsx nem uma pasta app/permissions/ no projeto. A funcionalidade de permissões do app é tratada por um modal global, PermissionModal, renderizado direto no layout raiz, não por uma rota. Esse Stack.Screen name="permissions" provavelmente é resquício de uma tela que existiu ou que foi planejada como rota e depois virou modal, sem que a declaração no Stack fosse removida. Declarar uma tela inexistente no Stack não quebra nada, o Expo Router simplesmente nunca vai conseguir navegar para ela.
Persistência da rota atual
RoutePersistence, montado logo no topo da árvore, é o componente que efetivamente chama saveLastVisitedRoute a cada mudança de pathname ou parâmetros, alimentando a restauração de rota já documentada em Guardas de Rota e Inicialização. Ele guarda uma referência da última rota já persistida para não escrever no storage repetidamente quando nada mudou de fato.
O que acontece quando o app volta para o primeiro plano
O layout raiz também reage a mudanças de estado do app (AppState), não só à navegação. Tanto na montagem inicial quanto toda vez que o app volta a ficar "active", dois serviços são acionados diretamente:
useEffect(() => {
if (!fontsLoaded || fontError || SHOW_STORYBOOK) return;
void backgroundKeepAliveService.start();
void saveSaleDraftService.processPendingQueue();
const subscription = AppState.addEventListener("change", (nextState) => {
if (nextState === "active") {
void backgroundKeepAliveService.start();
void saveSaleDraftService.processPendingQueue();
}
});
return () => {
subscription.remove();
void backgroundKeepAliveService.stop();
};
}, [fontError, fontsLoaded]);
processPendingQueue, documentado em Salvar e Enviar, normalmente é acionado pela fila de retry com intervalo configurável. Essa chamada direta no layout raiz é um segundo gatilho, independente daquele intervalo: sempre que o vendedor volta para o app depois de deixá-lo em segundo plano, a fila de envio é verificada imediatamente, sem esperar o próximo ciclo do timer.
Armadilhas conhecidas
Antes de adicionar uma nova rota ao Stack do layout raiz, confirme que o arquivo correspondente existe em app/. O caso de "permissions" mostra que uma entrada desatualizada pode continuar no código por tempo indefinido sem gerar nenhum erro visível.