Pular para o conteúdo principal

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:

CaminhoO 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.tsxCadastro 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.tsxTelas de detalhe/faturamento/pagamento de venda, fora do editor de rascunho
app/index.tsxGate 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.