graph TD
A["Tela Inicial<br/>(na base da pilha)"] --> B["Catálogo de Produtos<br/>(empilhada sobre a inicial)"]
B --> C["Detalhe do Produto<br/>(empilhada sobre o catálogo)"]
C --> D["Confirmação do Pedido<br/>(no topo da pilha — tela ativa)"]
style D fill:#1565C0,color:#fff
style C fill:#1976D2,color:#fff
style B fill:#1E88E5,color:#fff
style A fill:#42A5F5,color:#fff
- Conteúdo
- Navegação e Roteamento
- Material
Módulo 05 — Navegação e Roteamento
Até aqui, você construiu telas individuais com layouts sofisticados, aprendeu a linguagem Dart com profundidade e entendeu como o Flutter organiza widgets em árvore. Mas uma aplicação real raramente existe em uma única tela. O usuário navega: ele abre o catálogo de produtos, toca em um item específico para ver os detalhes, adiciona ao pedido, confirma a entrega, acessa o perfil. Cada uma dessas ações o leva a uma nova tela, e o caminho de volta precisa estar sempre disponível. Neste módulo, você vai aprender a conectar as telas da sua aplicação de forma organizada, robusta e escalável, utilizando o pacote go_router — a solução de navegação recomendada pela comunidade Flutter para aplicações de produção. Estude o material com calma, execute cada exemplo no seu ambiente e chegue à aula pronto para implementar a estrutura de navegação completa do Projeto Integrador.
A Metáfora da Pilha de Telas
Antes de escrever uma única linha de código relacionada à navegação, você precisa construir o modelo mental correto sobre como as telas de um aplicativo móvel se organizam. Esse modelo mental é simples, mas poderoso, e vai guiar suas decisões durante todo o módulo.
Imagine uma pilha de cartas sobre uma mesa. A carta que está no topo é a única que você vê. Quando você quer ver uma nova carta, você a coloca no topo da pilha. Quando você quer voltar para a carta anterior, você retira a carta do topo. Essa é exatamente a metáfora que governa a navegação em aplicativos móveis: cada tela que o usuário abre é empilhada sobre as anteriores, e o botão de voltar remove a tela do topo, revelando a que estava embaixo.
Esse modelo tem um nome formal: ele é chamado de pilha de navegação ou, em inglês, navigation stack. A tela que está no topo da pilha é a tela ativa, aquela que o usuário está vendo. As telas abaixo permanecem na memória, preservando seu estado, aguardando que o usuário volte para elas.
Esse comportamento de empilhamento é natural para os usuários de dispositivos móveis. Quando alguém toca em “Ver detalhes” em um produto, espera que o botão de voltar o leve de volta ao catálogo. Quando finaliza o pedido, espera ser levado à tela de confirmação. Quebrar essa expectativa — por exemplo, fazer o botão de voltar levar o usuário para a tela errada — é uma das formas mais rápidas de prejudicar a experiência do usuário.
Além da operação básica de empilhar e desempilhar, você vai precisar de outras operações ao longo do desenvolvimento do aplicativo. Algumas telas não devem ser mantidas na pilha quando o usuário avança. Por exemplo, após um login bem-sucedido, você não quer que o usuário consiga voltar para a tela de login pressionando o botão de voltar — ele já está autenticado, e voltar para o login seria confuso. Para esses casos, existe a operação de substituição de tela, onde a nova tela ocupa o lugar da anterior na pilha sem que a anterior seja mantida. E existe também a operação de limpeza de pilha, onde você navega para uma nova tela descartando todas as anteriores — útil, por exemplo, ao fazer logout, quando você quer que o usuário comece do zero na tela de login sem qualquer rastro da sessão anterior.
Navigator 1.0 — A API Imperativa Nativa do Flutter
O ponto de partida: entender a fundação antes de usar a abstração
O Flutter nasceu com uma API de navegação embutida, chamada informalmente de Navigator 1.0. Mesmo que você vá usar o go_router para gerenciar a navegação do Projeto Integrador, entender o Navigator 1.0 é importante por duas razões. Primeiro, ele é a fundação sobre a qual todas as soluções de navegação mais avançadas, incluindo o go_router, são construídas. Segundo, você vai encontrar código que usa o Navigator 1.0 em repositórios, tutoriais e bases de código existentes, e precisa saber ler e entender esse código.
O Navigator 1.0 é uma API imperativa: você chama métodos que manipulam a pilha de navegação diretamente. O objeto Navigator é acessível a partir do contexto com Navigator.of(context), e os métodos mais importantes são push, pop, pushReplacement e pushAndRemoveUntil.
O método push empurra uma nova rota para o topo da pilha. Uma rota, no vocabulário do Flutter, é um objeto que encapsula a tela e a transição de entrada e saída. A forma mais simples de criar uma rota é usando MaterialPageRoute, que produz a animação de transição padrão da plataforma — deslize horizontal no iOS, fade no Android.
Exemplo — Navegação básica com Navigator.push
import 'package:flutter/material.dart';
// Tela de origem: lista de produtos do aplicativo
class TelaCatalogo extends StatelessWidget {
final List<String> nomeProdutos;
const TelaCatalogo({super.key, required this.nomeProdutos});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: ListView.builder(
itemCount: nomeProdutos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(nomeProdutos[index]),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {
// Navigator.push empurra TelaDetalhe para o topo da pilha.
// MaterialPageRoute cria a animação padrão da plataforma.
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TelaDetalhe(
nomeProduto: nomeProdutos[index],
),
),
);
},
);
},
),
);
}
}
// Tela de destino: detalhe de um produto específico
class TelaDetalhe extends StatelessWidget {
final String nomeProduto;
const TelaDetalhe({super.key, required this.nomeProduto});
@override
Widget build(BuildContext context) {
return Scaffold(
// O AppBar gerado por Scaffold automaticamente inclui o botão de voltar
// quando há uma rota abaixo desta na pilha.
appBar: AppBar(title: Text(nomeProduto)),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
nomeProduto,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
ElevatedButton(
// Navigator.pop retira esta tela da pilha, voltando ao catálogo.
onPressed: () => Navigator.of(context).pop(),
child: const Text('Voltar ao catálogo'),
),
],
),
),
);
}
}import 'package:flutter/material.dart';
class TelaCatalogo extends StatelessWidget {
final List<String> nomeProdutos;
const TelaCatalogo({super.key, required this.nomeProdutos});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: ListView.builder(
itemCount: nomeProdutos.length,
itemBuilder: (context, index) => ListTile(
title: Text(nomeProdutos[index]),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => TelaDetalhe(nomeProduto: nomeProdutos[index]),
),
),
),
),
);
}
class TelaDetalhe extends StatelessWidget {
final String nomeProduto;
const TelaDetalhe({super.key, required this.nomeProduto});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(nomeProduto)),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(nomeProduto,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Voltar ao catálogo'),
),
],
),
),
);
}Observe algo importante no exemplo acima: os dados são passados para a nova tela diretamente via construtor. No Navigator 1.0, essa é a forma natural de transmitir informações de uma tela para outra — você simplesmente passa os valores como parâmetros ao instanciar o widget da tela de destino. Essa simplicidade é uma das vantagens do Navigator 1.0 para casos simples.
Recebendo dados de volta ao navegar
Além de passar dados para frente, é possível receber dados de volta quando uma tela é fechada com Navigator.pop. O método push retorna um Future que completa quando a tela é removida da pilha. Se o pop for chamado com um argumento, esse argumento se torna o resultado do Future.
Exemplo — Passando dados de volta com Navigator.pop
// Tela de seleção: o usuário escolhe um endereço e o resultado
// é retornado para a tela chamadora.
class TelaSelecionarEndereco extends StatelessWidget {
final List<String> enderecos;
const TelaSelecionarEndereco({super.key, required this.enderecos});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Selecionar endereço')),
body: ListView.builder(
itemCount: enderecos.length,
itemBuilder: (context, index) {
return ListTile(
leading: const Icon(Icons.location_on),
title: Text(enderecos[index]),
onTap: () {
// Passa o endereço selecionado de volta para a tela chamadora.
// Navigator.pop com argumento completa o Future retornado por push.
Navigator.of(context).pop(enderecos[index]);
},
);
},
),
);
}
}
// Na tela chamadora:
Future<void> _selecionarEndereco(BuildContext context) async {
// push retorna um Future<T?> — o resultado pode ser nulo se o usuário
// fechar a tela sem selecionar nada (por exemplo, tocando no botão voltar).
final enderecoSelecionado = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) => TelaSelecionarEndereco(
enderecos: ['Rua das Flores, 123', 'Av. Central, 456', 'Rua do Comércio, 789'],
),
),
);
// Verificação de nulidade: o usuário pode ter voltado sem selecionar.
if (enderecoSelecionado != null) {
// Aqui você atualizaria o estado com o endereço selecionado.
print('Endereço selecionado: $enderecoSelecionado');
}
}class TelaSelecionarEndereco extends StatelessWidget {
final List<String> enderecos;
const TelaSelecionarEndereco({super.key, required this.enderecos});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Selecionar endereço')),
body: ListView.builder(
itemCount: enderecos.length,
itemBuilder: (ctx, i) => ListTile(
leading: const Icon(Icons.location_on),
title: Text(enderecos[i]),
onTap: () => Navigator.of(ctx).pop(enderecos[i]),
),
),
);
}
Future<void> _selecionarEndereco(BuildContext context) async {
final selecionado = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (_) => TelaSelecionarEndereco(enderecos: const [
'Rua das Flores, 123',
'Av. Central, 456',
'Rua do Comércio, 789',
]),
),
);
if (selecionado != null) print('Selecionado: $selecionado');
}pushReplacement e pushAndRemoveUntil
O método pushReplacement substitui a tela atual no topo da pilha por uma nova, sem que a tela substituída seja mantida. Pense na situação após um login bem-sucedido: você quer que a tela principal apareça, mas não quer que o usuário consiga voltar para a tela de login pressionando o botão de voltar. O pushReplacement é exatamente para isso — ele remove a tela atual da pilha e a substitui pela nova.
Já o pushAndRemoveUntil vai além: ele empurra uma nova rota e remove todas as rotas existentes na pilha até que uma condição seja satisfeita. Se você passar uma função que sempre retorna false, todas as rotas serão removidas, e a nova rota ficará sozinha na pilha. Isso é o comportamento desejado no fluxo de logout, quando você quer levar o usuário para a tela de login sem qualquer possibilidade de voltar para as telas da sessão encerrada.
Exemplo — pushReplacement após login e pushAndRemoveUntil no logout
// Após login bem-sucedido: substitui a tela de login pela tela principal.
// O usuário não pode mais voltar para o login com o botão de voltar.
void _navegarAposDologin(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const TelaPrincipal()),
);
}
// No logout: vai para a tela de login e remove TODA a pilha.
// (ModalRoute.withName('/') ou simplesmente (route) => false)
void _navegarNoLogout(BuildContext context) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const TelaLogin()),
// A função predicate retorna true para as rotas que devem ser MANTIDAS.
// Retornar false para todas remove toda a pilha.
(route) => false,
);
}As Limitações do Navigator 1.0
O Navigator 1.0 funciona muito bem para casos simples, e você vai encontrá-lo frequentemente em exemplos introdutórios de Flutter. Mas à medida que a aplicação cresce em complexidade, as limitações dessa abordagem ficam evidentes. Entender essas limitações vai ajudá-lo a apreciar o que o go_router resolve.
A primeira limitação é a ausência de suporte nativo a deep linking e URLs. Deep linking é a capacidade de abrir o aplicativo diretamente em uma tela específica a partir de um link externo — por exemplo, uma notificação push que ao ser tocada abre diretamente a tela de detalhes de um pedido específico. Com o Navigator 1.0, implementar deep linking requer código manual e propenso a erros para analisar a URL e construir a pilha de navegação correspondente.
A segunda limitação é a dificuldade de rastrear e reconstruir o estado de navegação. No Navigator 1.0, o estado da pilha fica completamente dentro do widget Navigator, inacessível de fora. Isso dificulta a implementação de recursos como o botão de avançar e voltar do navegador web, a serialização do estado de navegação para restaurar a posição do usuário após reiniciar o aplicativo, e os testes automatizados de fluxos de navegação.
A terceira limitação é a forma como os dados são passados entre telas. Como você viu nos exemplos anteriores, os dados precisam ser passados como parâmetros do construtor diretamente ao criar o widget da tela de destino. Isso cria um acoplamento forte entre as telas: a tela A precisa saber exatamente quais parâmetros a tela B espera para poder instanciá-la. Em aplicações com muitas telas e dados complexos, essa dependência se torna difícil de gerenciar.
A quarta limitação é o suporte inadequado para navegação em abas e outras estruturas de navegação não lineares. Quando você tem uma BottomNavigationBar com três abas, cada aba deveria ter sua própria pilha de navegação independente, de forma que o usuário possa estar na tela de detalhes na aba de pedidos, trocar para a aba de perfil, e ao voltar para a aba de pedidos estar exatamente onde estava. Implementar isso com o Navigator 1.0 é possível, mas trabalhoso e sujeito a bugs.
O Navigator 2.0 foi introduzido para resolver essas limitações, e o go_router é a biblioteca que torna o uso do Navigator 2.0 acessível e prático.
Navigator 2.0 — A Abordagem Declarativa
O Navigator 2.0 representa uma mudança fundamental de paradigma: em vez de chamar métodos para manipular a pilha de navegação imperativa-mente, você declara qual deve ser o estado da pilha de acordo com o estado atual da aplicação. O roteador determina as rotas ativas a partir de uma URL ou de um estado de autenticação, e o Navigator exibe as telas correspondentes.
Essa abordagem declarativa é coerente com a filosofia geral do Flutter. Assim como o método build descreve como a interface deve parecer dado o estado atual — em vez de chamar métodos para mudar cada elemento da interface um por um — a navegação declarativa descreve quais telas devem estar ativas dado o estado da aplicação, em vez de chamar push e pop manualmente.
A API do Navigator 2.0 puro é mais verbosa e complexa do que o Navigator 1.0. Ela envolve a implementação de um RouterDelegate e de um RouteInformationParser, que são responsáveis por traduzir entre o estado da aplicação e a representação em URL, e vice-versa. Para a maioria das aplicações, implementar essas interfaces diretamente é desnecessariamente trabalhoso.
É exatamente aí que o go_router entra. Ele implementa o Navigator 2.0 internamente e expõe uma API limpa e intuitiva que elimina a necessidade de escrever o código de baixo nível. Você define suas rotas de forma declarativa com URLs, o go_router cuida da análise de URLs, do deep linking e da construção da pilha de navegação, e você se concentra apenas em definir quais telas pertencem a quais endereços e quais condições governam o acesso a cada uma.
Configurando o go_router no Projeto
Para usar o go_router, você precisa primeiro adicioná-lo como dependência no arquivo pubspec.yaml do projeto. A versão utilizada na disciplina é a ^17.0.1, que é compatível com as versões recentes do Flutter. Após adicionar a dependência, execute flutter pub get para baixar o pacote.
Com o pacote disponível, a configuração mínima do go_router consiste em criar uma instância de GoRouter, definir as rotas da aplicação e conectar o roteador ao MaterialApp. O lugar correto para criar o GoRouter é como um campo de nível superior ou dentro de uma classe que gerencia a configuração do aplicativo — não dentro de um build, pois isso recriaria o roteador a cada reconstrução do widget, causando perda do estado de navegação.
Exemplo — Configuração mínima do go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// A instância do GoRouter é criada fora do build para não ser recriada
// a cada reconstrução do widget raiz. Basta uma única instância por aplicativo.
final GoRouter _roteador = GoRouter(
// A rota inicial: onde o aplicativo começa ao ser aberto.
initialLocation: '/',
routes: [
// GoRoute define uma rota com um path (URL) e um builder (widget a exibir).
GoRoute(
path: '/',
// O builder recebe o contexto e o GoRouterState, que contém
// informações sobre a rota atual como parâmetros e query strings.
builder: (context, state) => const TelaPrincipal(),
),
GoRoute(
path: '/catalogo',
builder: (context, state) => const TelaCatalogo(),
),
GoRoute(
path: '/perfil',
builder: (context, state) => const TelaPerfil(),
),
GoRoute(
path: '/login',
builder: (context, state) => const TelaLogin(),
),
],
);
void main() {
runApp(const MeuApp());
}
class MeuApp extends StatelessWidget {
const MeuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Aplicativo de Pedidos',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
),
// Conecta o MaterialApp ao GoRouter.
// MaterialApp.router usa routerConfig em vez de home ou routes.
routerConfig: _roteador,
);
}
}
// Widgets de tela simplificados para o exemplo
class TelaPrincipal extends StatelessWidget {
const TelaPrincipal({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
// context.go navega para a rota /catalogo,
// substituindo a rota atual na pilha (como pushReplacement).
onPressed: () => context.go('/catalogo'),
child: const Text('Ver Catálogo'),
),
const SizedBox(height: 12),
ElevatedButton(
// context.push adiciona a rota à pilha,
// mantendo as rotas anteriores (como push).
onPressed: () => context.push('/perfil'),
child: const Text('Meu Perfil'),
),
],
),
),
);
}
class TelaCatalogo extends StatelessWidget {
const TelaCatalogo({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: const Center(child: Text('Lista de produtos aqui')),
);
}
class TelaPerfil extends StatelessWidget {
const TelaPerfil({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Perfil')),
body: const Center(child: Text('Dados do perfil aqui')),
);
}
class TelaLogin extends StatelessWidget {
const TelaLogin({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Login')),
body: const Center(child: Text('Formulário de login aqui')),
);
}import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const TelaPrincipal()),
GoRoute(path: '/catalogo', builder: (_, __) => const TelaCatalogo()),
GoRoute(path: '/perfil', builder: (_, __) => const TelaPerfil()),
GoRoute(path: '/login', builder: (_, __) => const TelaLogin()),
],
);
void main() => runApp(
MaterialApp.router(
title: 'Aplicativo de Pedidos',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)),
routerConfig: _roteador,
),
);
class TelaPrincipal extends StatelessWidget {
const TelaPrincipal({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
ElevatedButton(
onPressed: () => context.go('/catalogo'),
child: const Text('Ver Catálogo')),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => context.push('/perfil'),
child: const Text('Meu Perfil')),
]),
),
);
}
class TelaCatalogo extends StatelessWidget {
const TelaCatalogo({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: const Center(child: Text('Produtos aqui')));
}
class TelaPerfil extends StatelessWidget {
const TelaPerfil({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Perfil')),
body: const Center(child: Text('Perfil aqui')));
}
class TelaLogin extends StatelessWidget {
const TelaLogin({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Login')),
body: const Center(child: Text('Login aqui')));
}Observe a diferença entre MaterialApp e MaterialApp.router. O MaterialApp convencional usa a propriedade home ou routes para definir as telas. O MaterialApp.router usa routerConfig, que aceita uma instância de RouterConfig — que é exatamente o que o GoRouter implementa. A maioria das outras propriedades do MaterialApp, como theme, title e locale, funcionam da mesma forma em ambos.
Navegação Programática: go, push e pop
Uma das diferenças mais importantes do go_router em relação ao Navigator 1.0 é como você navega entre telas. Em vez de chamar Navigator.of(context).push(MaterialPageRoute(...)), você usa métodos de extensão que o go_router adiciona diretamente ao BuildContext. Essa sintaxe é mais concisa e mais expressiva.
O go_router adiciona três métodos principais ao BuildContext: context.go, context.push e context.pop. Cada um tem um comportamento distinto, e escolher o certo para cada situação é uma habilidade que você vai desenvolver com a prática.
O método context.go navega para uma rota substituindo toda a pilha de navegação atual. Ele é conceitualmente semelhante ao pushAndRemoveUntil do Navigator 1.0 com a condição sempre falsa: a URL muda, a tela muda, e não há forma de voltar para a tela anterior pelo botão de voltar. Use context.go quando a navegação representa uma transição de estado significativa — por exemplo, ao fazer login (indo de /login para /), ao completar um pedido (indo para a tela de confirmação de forma definitiva) ou ao fazer logout (voltando para /login).
O método context.push adiciona uma nova rota ao topo da pilha, preservando as rotas anteriores. O botão de voltar funcionará normalmente, levando o usuário de volta à rota anterior. Use context.push quando a nova tela é acessada em um fluxo que tem retorno natural — ver o detalhe de um produto, editar um endereço, visualizar uma notificação.
O método context.pop remove a rota do topo da pilha, voltando para a rota anterior. Ele é o equivalente ao Navigator.of(context).pop() do Navigator 1.0. Diferentemente do Navigator 1.0, o go_router também oferece context.pop(resultado), que permite retornar um valor para a tela anterior quando usado em conjunto com context.push.
Exemplo — Diferenças práticas entre go e push
class TelaPrincipalApp extends StatelessWidget {
const TelaPrincipalApp({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// context.go: navega substituindo a pilha.
// Após navegar para /catalogo, o botão de voltar NÃO aparece,
// pois não há rota abaixo de /catalogo na pilha.
ElevatedButton(
onPressed: () => context.go('/catalogo'),
child: const Text('Ir para Catálogo (sem voltar)'),
),
const SizedBox(height: 12),
// context.push: empilha /perfil sobre a rota atual.
// O botão de voltar APARECE, pois a tela inicial ainda está na pilha.
OutlinedButton(
onPressed: () => context.push('/perfil'),
child: const Text('Abrir Perfil (com voltar)'),
),
const SizedBox(height: 12),
// Rotas nomeadas: alternativa mais segura que strings brutas.
// Evita typos ao referenciar rotas — o IDE alerta se o nome não existir.
ElevatedButton(
onPressed: () => context.goNamed('login'),
child: const Text('Sair (Logout)'),
),
],
),
),
);
}
}class TelaPrincipalApp extends StatelessWidget {
const TelaPrincipalApp({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () => context.go('/catalogo'),
child: const Text('Ir para Catálogo (sem voltar)')),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => context.push('/perfil'),
child: const Text('Abrir Perfil (com voltar)')),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => context.goNamed('login'),
child: const Text('Sair (Logout)')),
],
),
),
);
}Parâmetros de Rota: Passando Dados pela URL
Uma das funcionalidades mais poderosas do go_router é a capacidade de embutir dados diretamente na URL da rota. Isso resolve o problema de acoplamento do Navigator 1.0, onde a tela chamadora precisava instanciar o widget da tela de destino diretamente. Com parâmetros na URL, a tela de destino sabe como obter seus próprios dados a partir do GoRouterState.
O go_router suporta dois tipos de parâmetros: os path parameters, que fazem parte do caminho da URL, e os query parameters, que ficam após o ? na URL.
Os path parameters são definidos no padrão da rota com dois pontos antes do nome do parâmetro. Por exemplo, path: '/produto/:id' define uma rota onde :id é um segmento dinâmico da URL. Quando você navega para /produto/p001, o go_router extrai o valor p001 e o disponibiliza via state.pathParameters['id']. Os path parameters são adequados para identificadores que identificam o recurso que está sendo acessado — o ID de um produto, o número de um pedido.
Os query parameters ficam após o ? na URL e têm a forma chave=valor. Eles são adequados para dados de filtragem, ordenação ou configuração que não fazem parte da identidade do recurso. Por exemplo, /catalogo?categoria=Lanches&ordenar=preco indica que o catálogo deve ser filtrado pela categoria “Lanches” e ordenado por preço. Os query parameters são acessados via state.uri.queryParameters['categoria'].
Exemplo — Path parameters e query parameters no Projeto Integrador
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GoRouter _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const TelaPrincipal(),
),
GoRoute(
// :id é um path parameter — parte dinâmica do caminho.
// A URL /produto/p001 fará state.pathParameters['id'] == 'p001'.
path: '/produto/:id',
builder: (context, state) {
// Extrai o ID do produto a partir dos path parameters.
// O operador ! é seguro aqui porque a rota garante que 'id' existe
// sempre que este builder for chamado.
final idProduto = state.pathParameters['id']!;
return TelaDetalheProduto(idProduto: idProduto);
},
),
GoRoute(
path: '/catalogo',
builder: (context, state) {
// Query parameters: /catalogo?categoria=Lanches&ordenar=preco
// Podem ser nulos se não forem fornecidos na URL.
final categoria = state.uri.queryParameters['categoria'];
final ordenar = state.uri.queryParameters['ordenar'];
return TelaCatalogo(
categoriaFiltro: categoria,
ordenacao: ordenar,
);
},
),
],
);
// Na tela principal, você navega para o detalhe de um produto assim:
class TelaPrincipal extends StatelessWidget {
const TelaPrincipal({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Column(
children: [
// Navegação para produto com path parameter
ElevatedButton(
onPressed: () => context.go('/produto/p001'),
child: const Text('Ver X-Burguer'),
),
// Navegação com query parameters
ElevatedButton(
onPressed: () => context.go('/catalogo?categoria=Lanches&ordenar=preco'),
child: const Text('Ver Lanches por preço'),
),
// Usando goNamed com pathParameters e queryParameters
ElevatedButton(
onPressed: () => context.goNamed(
'produto',
pathParameters: {'id': 'p002'},
),
child: const Text('Ver X-Salada'),
),
],
),
);
}
}
class TelaDetalheProduto extends StatelessWidget {
final String idProduto;
const TelaDetalheProduto({super.key, required this.idProduto});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Produto $idProduto')),
body: Center(
child: Text(
'Carregando produto com ID: $idProduto',
style: const TextStyle(fontSize: 18),
),
),
);
}
}
class TelaCatalogo extends StatelessWidget {
final String? categoriaFiltro;
final String? ordenacao;
const TelaCatalogo({super.key, this.categoriaFiltro, this.ordenacao});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Categoria: ${categoriaFiltro ?? 'Todas'}'),
Text('Ordenação: ${ordenacao ?? 'Padrão'}'),
],
),
),
);
}
}import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const TelaPrincipal()),
GoRoute(
name: 'produto',
path: '/produto/:id',
builder: (_, s) => TelaDetalheProduto(idProduto: s.pathParameters['id']!),
),
GoRoute(
path: '/catalogo',
builder: (_, s) => TelaCatalogo(
categoriaFiltro: s.uri.queryParameters['categoria'],
ordenacao: s.uri.queryParameters['ordenar'],
),
),
],
);
class TelaPrincipal extends StatelessWidget {
const TelaPrincipal({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Column(children: [
ElevatedButton(
onPressed: () => context.go('/produto/p001'),
child: const Text('Ver X-Burguer')),
ElevatedButton(
onPressed: () =>
context.go('/catalogo?categoria=Lanches&ordenar=preco'),
child: const Text('Ver Lanches por preço')),
ElevatedButton(
onPressed: () =>
context.goNamed('produto', pathParameters: {'id': 'p002'}),
child: const Text('Ver X-Salada')),
]),
);
}
class TelaDetalheProduto extends StatelessWidget {
final String idProduto;
const TelaDetalheProduto({super.key, required this.idProduto});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Produto $idProduto')),
body: Center(child: Text('ID: $idProduto')));
}
class TelaCatalogo extends StatelessWidget {
final String? categoriaFiltro, ordenacao;
const TelaCatalogo({super.key, this.categoriaFiltro, this.ordenacao});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Catálogo')),
body: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text('Categoria: ${categoriaFiltro ?? 'Todas'}'),
Text('Ordenação: ${ordenacao ?? 'Padrão'}'),
])));
}Há um terceiro mecanismo para passar dados: o parâmetro extra do GoRouterState. Com ele, você pode passar qualquer objeto Dart diretamente, sem precisar serializar em uma string de URL. Use context.go('/rota', extra: meuObjeto) para passar e state.extra as MinhaClasse para receber. No entanto, o extra não é serializável e não funciona com deep linking — se a URL for compartilhada ou acessada via notificação, o extra não estará disponível. Por isso, reserve o extra apenas para navegações internas onde a URL não será compartilhada.
Sub-rotas Aninhadas
O go_router suporta sub-rotas aninhadas, que permitem organizar a hierarquia de rotas de forma que reflita a hierarquia de telas da aplicação. Uma sub-rota é uma rota que existe dentro de outra rota, e sua URL é construída concatenando o path da rota pai com o path da sub-rota.
Por exemplo, se você tem uma rota /pedido que representa os pedidos do usuário, pode ter sub-rotas como /pedido/:id para o detalhe de um pedido específico e /pedido/:id/acompanhar para o rastreamento daquele pedido. Isso cria uma estrutura hierárquica que é intuitiva tanto para o desenvolvedor quanto para o usuário.
Exemplo — Sub-rotas hierárquicas de pedidos
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GoRouter _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const TelaPrincipal(),
),
// Rota pai: lista de pedidos
GoRoute(
path: '/pedido',
builder: (context, state) => const TelaListaPedidos(),
// routes: lista de sub-rotas que são filhas desta rota.
// A URL completa é /pedido + sub-path.
routes: [
// Sub-rota: detalhe de um pedido específico (/pedido/:id)
GoRoute(
path: ':id',
builder: (context, state) {
final idPedido = state.pathParameters['id']!;
return TelaDetalhePedido(idPedido: idPedido);
},
// Sub-sub-rota: acompanhamento do pedido (/pedido/:id/acompanhar)
routes: [
GoRoute(
path: 'acompanhar',
builder: (context, state) {
final idPedido = state.pathParameters['id']!;
return TelaAcompanharPedido(idPedido: idPedido);
},
),
],
),
],
),
],
);
// Navegação para as sub-rotas usa a URL completa
class TelaListaPedidos extends StatelessWidget {
const TelaListaPedidos({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Meus Pedidos')),
body: ListView(
children: [
ListTile(
title: const Text('Pedido #2024001'),
subtitle: const Text('X-Burguer + Batata Frita'),
onTap: () => context.go('/pedido/2024001'),
),
ListTile(
title: const Text('Pedido #2024002'),
subtitle: const Text('Suco de Laranja x2'),
onTap: () => context.go('/pedido/2024002'),
),
],
),
);
}
}
class TelaDetalhePedido extends StatelessWidget {
final String idPedido;
const TelaDetalhePedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pedido $idPedido')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Detalhes do pedido $idPedido'),
const SizedBox(height: 16),
ElevatedButton(
// Navega para a sub-sub-rota de acompanhamento
onPressed: () => context.go('/pedido/$idPedido/acompanhar'),
child: const Text('Acompanhar entrega'),
),
],
),
),
);
}
}
class TelaAcompanharPedido extends StatelessWidget {
final String idPedido;
const TelaAcompanharPedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Acompanhando #$idPedido')),
body: const Center(child: Text('Mapa de rastreamento aqui')),
);
}import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const TelaPrincipal()),
GoRoute(
path: '/pedido',
builder: (_, __) => const TelaListaPedidos(),
routes: [
GoRoute(
path: ':id',
builder: (_, s) => TelaDetalhePedido(idPedido: s.pathParameters['id']!),
routes: [
GoRoute(
path: 'acompanhar',
builder: (_, s) =>
TelaAcompanharPedido(idPedido: s.pathParameters['id']!),
),
],
),
],
),
],
);
class TelaListaPedidos extends StatelessWidget {
const TelaListaPedidos({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Meus Pedidos')),
body: ListView(children: [
ListTile(
title: const Text('Pedido #2024001'),
onTap: () => context.go('/pedido/2024001')),
ListTile(
title: const Text('Pedido #2024002'),
onTap: () => context.go('/pedido/2024002')),
]),
);
}
class TelaDetalhePedido extends StatelessWidget {
final String idPedido;
const TelaDetalhePedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Pedido $idPedido')),
body: Center(
child: ElevatedButton(
onPressed: () => context.go('/pedido/$idPedido/acompanhar'),
child: const Text('Acompanhar entrega'),
),
),
);
}
class TelaAcompanharPedido extends StatelessWidget {
final String idPedido;
const TelaAcompanharPedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Acompanhando #$idPedido')),
body: const Center(child: Text('Rastreamento aqui')));
}Redirect — Guardas de Autenticação
Um dos recursos mais importantes para aplicações com autenticação
A maioria das aplicações possui telas que somente podem ser acessadas por usuários autenticados. Um usuário que não está logado não deve conseguir acessar o catálogo, ver seus pedidos ou editar o perfil — ele deve ser redirecionado para a tela de login. O mecanismo de redirect do go_router é a ferramenta certa para implementar esse comportamento de forma centralizada e declarativa.
O redirect é uma função que o go_router chama antes de exibir qualquer rota. Ela recebe o contexto e o GoRouterState atual, e pode retornar uma string de URL para redirecionar o usuário, ou null para permitir que a navegação continue normalmente. Se a função retornar uma URL, o go_router navega automaticamente para essa URL em vez da original.
A função de redirect é chamada em dois momentos: quando o usuário inicia a aplicação e quando tenta navegar para uma rota. Isso garante que um usuário que acabou de deslogar e tenta navegar diretamente para uma rota protegida seja redirecionado corretamente.
flowchart TD
A[Usuário tenta navegar para /catalogo] --> B{redirect chamado}
B --> C{Usuário autenticado?}
C -- Sim --> D["redirect retorna null<br/>(navegação continua)"]
C -- Não --> E["redirect retorna '/login'"]
D --> F[/catalogo é exibido]
E --> G[/login é exibido]
G --> H{Login bem-sucedido?}
H -- Sim --> I["context.go('/')"]
H -- Não --> G
Exemplo — Guard de autenticação com redirect
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Serviço de autenticação simples para o exemplo.
// Em produção, este estado viria de um Provider ou de um service locator (GetIt).
class ServicoAutenticacao extends ChangeNotifier {
bool _autenticado = false;
bool get autenticado => _autenticado;
void login() {
_autenticado = true;
notifyListeners();
}
void logout() {
_autenticado = false;
notifyListeners();
}
}
// Instância global para o exemplo. Em produção, use GetIt ou Provider.
final servicoAuth = ServicoAutenticacao();
final GoRouter _roteador = GoRouter(
initialLocation: '/',
// refreshListenable: instrui o go_router a reavaliar o redirect
// sempre que este ChangeNotifier notificar uma mudança.
// Quando o usuário faz login ou logout, o go_router chama redirect novamente.
refreshListenable: servicoAuth,
redirect: (BuildContext context, GoRouterState state) {
final autenticado = servicoAuth.autenticado;
final naRotaDeLogin = state.matchedLocation == '/login';
// Se não está autenticado e não está na tela de login, redireciona para login.
if (!autenticado && !naRotaDeLogin) {
return '/login';
}
// Se está autenticado e está na tela de login, redireciona para a tela principal.
// Isso evita que um usuário já logado acesse o login novamente.
if (autenticado && naRotaDeLogin) {
return '/';
}
// null: nenhum redirecionamento necessário, permite a navegação normal.
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const TelaPrincipalAutenticada(),
),
GoRoute(
path: '/catalogo',
builder: (context, state) => const TelaCatalogo(),
),
GoRoute(
path: '/perfil',
builder: (context, state) => const TelaPerfil(),
),
GoRoute(
path: '/login',
builder: (context, state) => const TelaLogin(),
),
],
);
class TelaLogin extends StatelessWidget {
const TelaLogin({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: ElevatedButton(
onPressed: () {
// Ao fazer login, o refreshListenable notifica o go_router,
// que reavaleia o redirect. Como agora autenticado == true
// e estamos em /login, o redirect redireciona para /.
servicoAuth.login();
},
child: const Text('Entrar'),
),
),
);
}
}
class TelaPrincipalAutenticada extends StatelessWidget {
const TelaPrincipalAutenticada({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Bem-vindo! Você está autenticado.'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// Ao fazer logout, o redirect redirecionará para /login automaticamente.
servicoAuth.logout();
},
child: const Text('Sair'),
),
],
),
),
);
}
}import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class ServicoAutenticacao extends ChangeNotifier {
bool _autenticado = false;
bool get autenticado => _autenticado;
void login() => _updateAuth(true);
void logout() => _updateAuth(false);
void _updateAuth(bool valor) {
_autenticado = valor;
notifyListeners();
}
}
final _auth = ServicoAutenticacao();
final _roteador = GoRouter(
initialLocation: '/',
refreshListenable: _auth,
redirect: (_, state) {
final logado = _auth.autenticado;
final noLogin = state.matchedLocation == '/login';
if (!logado && !noLogin) return '/login';
if (logado && noLogin) return '/';
return null;
},
routes: [
GoRoute(path: '/', builder: (_, __) => const TelaPrincipalAutenticada()),
GoRoute(path: '/catalogo', builder: (_, __) => const TelaCatalogo()),
GoRoute(path: '/login', builder: (_, __) => const TelaLogin()),
],
);
class TelaLogin extends StatelessWidget {
const TelaLogin({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child:
ElevatedButton(onPressed: _auth.login, child: const Text('Entrar')),
),
);
}
class TelaPrincipalAutenticada extends StatelessWidget {
const TelaPrincipalAutenticada({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: Center(
child: ElevatedButton(onPressed: _auth.logout, child: const Text('Sair')),
),
);
}O detalhe mais importante neste exemplo é o refreshListenable. Sem ele, o go_router avaliaria o redirect somente quando a rota mudasse, mas não quando o estado de autenticação mudasse. Com o refreshListenable, toda vez que servicoAuth.notifyListeners() é chamado — no login e no logout — o go_router reavalia o redirect e navega para a rota correta automaticamente. Você não precisa chamar context.go manualmente após o login; o redirect cuida disso.
ShellRoute — Elementos de Navegação Persistentes
Um padrão de interface extremamente comum em aplicações móveis é a BottomNavigationBar: uma barra no rodapé da tela com ícones para as seções principais do aplicativo. Quando o usuário troca de aba, o conteúdo central muda, mas a barra de navegação permanece visível. Implementar esse comportamento corretamente — com o go_router gerenciando as rotas e a barra persistindo entre elas — é uma das tarefas mais importantes deste módulo.
O ShellRoute é o widget do go_router que resolve esse problema. Ele envolve um conjunto de rotas em um “shell”, que é um widget pai que persiste enquanto o usuário navega entre as rotas filhas. A BottomNavigationBar é colocada nesse shell, e o conteúdo de cada rota filha é exibido em uma área central do shell.
A diferença entre ShellRoute e StatefulShellRoute é importante. O ShellRoute simples reconstrói o conteúdo de cada aba ao trocar. O StatefulShellRoute, introduzido nas versões mais recentes do go_router, preserva o estado de cada aba quando o usuário troca entre elas — exatamente como o usuário espera que funcione. Se você estava na tela de detalhes de um produto dentro da aba de catálogo, ao voltar para essa aba você vai encontrá-la exatamente como deixou. O StatefulShellRoute.indexedStack é a variante mais comum do StatefulShellRoute.
Exemplo — StatefulShellRoute com BottomNavigationBar persistente
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// As três abas principais do aplicativo de pedidos
final _abas = [
_ConfiguracaoAba(rotaInicial: '/inicio', icone: Icons.home, rotulo: 'Início'),
_ConfiguracaoAba(rotaInicial: '/pedidos', icone: Icons.receipt_long, rotulo: 'Pedidos'),
_ConfiguracaoAba(rotaInicial: '/perfil', icone: Icons.person, rotulo: 'Perfil'),
];
class _ConfiguracaoAba {
final String rotaInicial;
final IconData icone;
final String rotulo;
const _ConfiguracaoAba({
required this.rotaInicial,
required this.icone,
required this.rotulo,
});
}
final GoRouter _roteador = GoRouter(
initialLocation: '/inicio',
routes: [
// StatefulShellRoute.indexedStack mantém o estado de cada aba.
// O 'builder' recebe o contexto, o estado e o 'navigationShell',
// que é o widget do conteúdo da aba ativa.
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return TelaBase(navigationShell: navigationShell);
},
// branches: cada branch é uma aba independente com suas próprias rotas.
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/inicio',
builder: (context, state) => const TelaInicio(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/pedidos',
builder: (context, state) => const TelaListaPedidos(),
routes: [
// Sub-rota da aba de pedidos: mantida dentro da aba
GoRoute(
path: ':id',
builder: (context, state) => TelaDetalhePedido(
idPedido: state.pathParameters['id']!,
),
),
],
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/perfil',
builder: (context, state) => const TelaPerfil(),
),
],
),
],
),
],
);
// A tela base: persiste enquanto o usuário navega entre as abas.
// Ela contém o Scaffold com a BottomNavigationBar.
class TelaBase extends StatelessWidget {
// navigationShell é o widget que exibe o conteúdo da aba ativa.
final StatefulNavigationShell navigationShell;
const TelaBase({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
// O navigationShell é o corpo: ele exibe o widget da aba ativa.
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
// currentIndex: índice da aba ativa, vindo do navigationShell.
currentIndex: navigationShell.currentIndex,
onTap: (indice) {
// goBranch navega para a aba correspondente ao índice.
// initialLocation: true faz a aba voltar para sua rota inicial
// quando o usuário toca no ícone já estando nela (como no iOS).
navigationShell.goBranch(
indice,
initialLocation: indice == navigationShell.currentIndex,
);
},
items: _abas
.map((aba) => BottomNavigationBarItem(
icon: Icon(aba.icone),
label: aba.rotulo,
))
.toList(),
),
);
}
}
// Widgets das abas (simplificados para o exemplo)
class TelaInicio extends StatelessWidget {
const TelaInicio({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: const Center(child: Text('Tela inicial')),
);
}
class TelaListaPedidos extends StatelessWidget {
const TelaListaPedidos({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Meus Pedidos')),
body: ListView(
children: [
ListTile(
title: const Text('Pedido #2024001'),
onTap: () => context.go('/pedidos/2024001'),
),
],
),
);
}
class TelaDetalhePedido extends StatelessWidget {
final String idPedido;
const TelaDetalhePedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) => Scaffold(
// Ao navegar para o detalhe dentro da aba de pedidos,
// a BottomNavigationBar continua visível porque o Scaffold está no TelaBase.
appBar: AppBar(title: Text('Pedido $idPedido')),
body: Center(child: Text('Detalhes do pedido $idPedido')),
);
}
class TelaPerfil extends StatelessWidget {
const TelaPerfil({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Perfil')),
body: const Center(child: Text('Perfil do usuário')),
);
}
void main() => runApp(MaterialApp.router(
routerConfig: _roteador,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
),
));import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
const _naviItems = [
(icone: Icons.home, rotulo: 'Início'),
(icone: Icons.receipt_long, rotulo: 'Pedidos'),
(icone: Icons.person, rotulo: 'Perfil'),
];
final _roteador = GoRouter(
initialLocation: '/inicio',
routes: [
StatefulShellRoute.indexedStack(
builder: (_, __, shell) => TelaBase(shell: shell),
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/inicio', builder: (_, __) => const TelaInicio()),
]),
StatefulShellBranch(routes: [
GoRoute(
path: '/pedidos',
builder: (_, __) => const TelaListaPedidos(),
routes: [
GoRoute(
path: ':id',
builder: (_, s) =>
TelaDetalhePedido(idPedido: s.pathParameters['id']!)),
],
),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/perfil', builder: (_, __) => const TelaPerfil()),
]),
],
),
],
);
class TelaBase extends StatelessWidget {
final StatefulNavigationShell shell;
const TelaBase({super.key, required this.shell});
@override
Widget build(BuildContext context) => Scaffold(
body: shell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: shell.currentIndex,
onTap: (i) => shell.goBranch(i, initialLocation: i == shell.currentIndex),
items: _naviItems
.map((n) =>
BottomNavigationBarItem(icon: Icon(n.icone), label: n.rotulo))
.toList(),
),
);
}
class TelaInicio extends StatelessWidget {
const TelaInicio({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Início')),
body: const Center(child: Text('Tela inicial')));
}
class TelaListaPedidos extends StatelessWidget {
const TelaListaPedidos({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Pedidos')),
body: ListView(children: [
ListTile(
title: const Text('Pedido #2024001'),
onTap: () => context.go('/pedidos/2024001')),
]),
);
}
class TelaDetalhePedido extends StatelessWidget {
final String idPedido;
const TelaDetalhePedido({super.key, required this.idPedido});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Pedido $idPedido')),
body: Center(child: Text('Detalhe: $idPedido')));
}
class TelaPerfil extends StatelessWidget {
const TelaPerfil({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Perfil')),
body: const Center(child: Text('Perfil do usuário')));
}
void main() => runApp(MaterialApp.router(
routerConfig: _roteador,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange))));Preste atenção ao comportamento de goBranch com initialLocation: i == shell.currentIndex. Quando o usuário toca no ícone da aba que já está ativa, essa condição é verdadeira, e a aba volta para sua rota inicial. Isso imita o comportamento que os usuários esperam em aplicações iOS, onde tocar em uma aba ativa volta ao topo da lista dessa aba.
Deep Linking no Android
O deep linking permite que um link externo — como meuapp://produto/p001 ou https://meuapp.com/produto/p001 — abra o aplicativo diretamente na tela correspondente, passando o caminho da URL diretamente para o go_router. Esse recurso é utilizado, por exemplo, quando uma notificação push inclui um link para o produto que acaba de receber promoção, e ao tocar na notificação o usuário é levado diretamente à tela de detalhes daquele produto específico.
Para configurar o deep linking no Android, é necessário adicionar um intent filter no arquivo android/app/src/main/AndroidManifest.xml. Esse intent filter informa ao sistema operacional Android que o aplicativo é capaz de lidar com URLs de um determinado esquema ou host.
Exemplo — Configuração de deep linking no AndroidManifest.xml
<!-- Dentro da tag <activity> no AndroidManifest.xml -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Esquema customizado: abre com meuapp://qualquer/caminho -->
<data android:scheme="meuapp" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- HTTPS com host: abre com https://meuapp.com/qualquer/caminho -->
<data android:scheme="https" android:host="meuapp.com" />
</intent-filter>Com o intent filter configurado, o go_router analisa automaticamente a URL recebida e navega para a rota correspondente, passando os parâmetros corretos. Se você receber o deep link meuapp://produto/p001, o go_router vai para a rota /produto/p001 e o state.pathParameters['id'] terá o valor 'p001'.
Você não precisa escrever nenhum código Dart adicional para o deep linking funcionar com o go_router — a configuração do AndroidManifest.xml é suficiente. O go_router integra automaticamente o tratamento de intents do Android.
Para testar o deep linking durante o desenvolvimento com um emulador Android, você pode usar o comando adb shell am start -a android.intent.action.VIEW -d "meuapp://produto/p001" com.exemplo.meuapp a partir do terminal. Substitua com.exemplo.meuapp pelo package name do seu projeto, encontrado no AndroidManifest.xml.
Tratamento de Erros e Rotas Desconhecidas
Quando um usuário tenta acessar uma URL que não corresponde a nenhuma rota definida no go_router — seja por um deep link inválido, um link compartilhado desatualizado ou um erro de digitação —, o aplicativo precisa exibir uma tela de erro apropriada em vez de travar. O go_router oferece o parâmetro errorBuilder exatamente para esse propósito.
Exemplo — Tela de erro para rotas desconhecidas
final GoRouter _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (context, state) => const TelaPrincipal()),
// ... demais rotas
],
// errorBuilder: chamado quando nenhuma rota corresponde à URL solicitada.
errorBuilder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text('Página não encontrada')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
'A página "${state.uri}" não foi encontrada.',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('Voltar ao início'),
),
],
),
),
);
},
);final _roteador = GoRouter(
initialLocation: '/',
routes: [GoRoute(path: '/', builder: (_, __) => const TelaPrincipal())],
errorBuilder: (context, state) => Scaffold(
appBar: AppBar(title: const Text('Não encontrado')),
body: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.error_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text('"${state.uri}" não foi encontrada.',
textAlign: TextAlign.center),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/'), child: const Text('Início')),
]),
),
),
);Transições Customizadas entre Telas
Por padrão, o go_router utiliza as transições de tela padrão de cada plataforma: no Android, as telas entram com uma animação de fade combinada com deslize vertical; no iOS, entram deslizando da direita para a esquerda. Em muitas situações, você vai querer customizar essas transições para criar uma experiência mais alinhada com a identidade visual do seu aplicativo.
O CustomTransitionPage permite que você defina a animação de entrada e saída de uma rota de forma completamente livre. Você especifica um transitionsBuilder, que recebe o contexto, a Animation<double> da transição, a animação secundária (da rota que está sendo coberta) e o widget filho, e retorna o widget resultante da transição.
Exemplo — Transição de fade e de deslize personalizado
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final GoRouter _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const TelaPrincipal(),
),
GoRoute(
path: '/catalogo',
// Em vez de builder, use pageBuilder para controlar a transição.
// pageBuilder retorna um Page<void> em vez de um Widget.
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: const TelaCatalogo(),
// transitionsBuilder define a animação.
// animation: a animação da rota entrando (0.0 → 1.0).
// secondaryAnimation: a animação da rota saindo.
// child: o widget da nova tela.
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// FadeTransition: a nova tela aparece gradualmente.
return FadeTransition(
opacity: animation,
child: child,
);
},
// transitionDuration: duração da animação.
transitionDuration: const Duration(milliseconds: 300),
);
},
),
GoRoute(
path: '/confirmacao',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: const TelaConfirmacaoPedido(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// SlideTransition: a nova tela desliza de baixo para cima.
// Ideal para telas de confirmação ou modais de sucesso.
final offset = Tween<Offset>(
begin: const Offset(0.0, 1.0), // começa fora da tela, abaixo
end: Offset.zero, // termina na posição normal
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
));
return SlideTransition(
position: offset,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 400),
);
},
),
],
);import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
Page<void> _fadePage(GoRouterState state, Widget child) => CustomTransitionPage(
key: state.pageKey,
child: child,
transitionsBuilder: (_, animation, __, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: const Duration(milliseconds: 300),
);
Page<void> _slidePage(GoRouterState state, Widget child) => CustomTransitionPage(
key: state.pageKey,
child: child,
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: Tween(begin: const Offset(0.0, 1.0), end: Offset.zero)
.animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)),
child: child,
),
transitionDuration: const Duration(milliseconds: 400),
);
final _roteador = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const TelaPrincipal()),
GoRoute(
path: '/catalogo',
pageBuilder: (_, state) => _fadePage(state, const TelaCatalogo()),
),
GoRoute(
path: '/confirmacao',
pageBuilder: (_, state) => _slidePage(state, const TelaConfirmacaoPedido()),
),
],
);Organizando as Rotas em Produção: O Padrão AppRouter
À medida que o aplicativo cresce, a definição de todas as rotas em um único arquivo pode se tornar difícil de gerenciar. Uma abordagem que a comunidade Flutter adota amplamente é centralizar as definições de rotas em uma classe dedicada, frequentemente chamada de AppRouter ou AppRoutes. Essa classe reúne as constantes de nomes e caminhos de rotas e a instância do GoRouter, tornando o código organizado e fácil de manter.
Centralizar as rotas em uma classe dedicada traz duas vantagens imediatas. A primeira é a eliminação de strings literais espalhadas pelo código: em vez de escrever context.go('/produto/p001') em vinte lugares diferentes do aplicativo, você escreve context.go(AppRoutes.produto('p001')). Se o caminho da rota mudar, você altera em um único lugar. A segunda vantagem é a documentação implícita: ao olhar para a classe AppRoutes, você tem uma visão completa de todas as rotas do aplicativo sem precisar rastrear arquivos.
Exemplo — Classe AppRoutes para o Projeto Integrador
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// AppRoutes: centraliza todos os caminhos e nomes de rotas do aplicativo.
// Evita strings literais espalhadas e facilita refatoração.
abstract class AppRoutes {
// Rotas estáticas: caminhos que não dependem de parâmetros
static const String inicio = '/inicio';
static const String catalogo = '/catalogo';
static const String pedidos = '/pedidos';
static const String perfil = '/perfil';
static const String login = '/login';
static const String confirmacaoPedido = '/confirmacao';
// Rotas dinâmicas: funções que constroem o caminho com o parâmetro
static String detalheProduto(String idProduto) => '/produto/$idProduto';
static String detalhePedido(String idPedido) => '/pedidos/$idPedido';
// Nomes das rotas (para goNamed e pushNamed)
static const String nomeInicio = 'inicio';
static const String nomeProduto = 'produto';
static const String nomePedido = 'pedido';
static const String nomeLogin = 'login';
}
// AppRouter: cria e gerencia a instância do GoRouter.
// Separa a configuração do roteador do restante do código da aplicação.
class AppRouter {
// A instância é criada uma única vez e reutilizada.
static final GoRouter _roteador = GoRouter(
initialLocation: AppRoutes.inicio,
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
TelaBase(navigationShell: navigationShell),
branches: [
StatefulShellBranch(routes: [
GoRoute(
name: AppRoutes.nomeInicio,
path: AppRoutes.inicio,
builder: (context, state) => const TelaInicio(),
),
]),
StatefulShellBranch(routes: [
GoRoute(
path: AppRoutes.pedidos,
builder: (context, state) => const TelaListaPedidos(),
routes: [
GoRoute(
name: AppRoutes.nomePedido,
path: ':id',
builder: (context, state) => TelaDetalhePedido(
idPedido: state.pathParameters['id']!,
),
),
],
),
]),
StatefulShellBranch(routes: [
GoRoute(
path: AppRoutes.perfil,
builder: (context, state) => const TelaPerfil(),
),
]),
],
),
GoRoute(
name: AppRoutes.nomeLogin,
path: AppRoutes.login,
builder: (context, state) => const TelaLogin(),
),
GoRoute(
path: '/produto/:id',
name: AppRoutes.nomeProduto,
builder: (context, state) => TelaDetalheProduto(
idProduto: state.pathParameters['id']!,
),
),
],
errorBuilder: (context, state) => const TelaNaoEncontrada(),
);
// Exposição do roteador para uso no MaterialApp.router
static GoRouter get roteador => _roteador;
}
// Uso na função main
void main() {
runApp(
MaterialApp.router(
title: 'Aplicativo de Pedidos',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
),
routerConfig: AppRouter.roteador,
),
);
}
// Exemplo de navegação usando AppRoutes
class ExemploNavegacao extends StatelessWidget {
const ExemploNavegacao({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
// Navegação para produto com função auxiliar de AppRoutes
onPressed: () => context.go(AppRoutes.detalheProduto('p001')),
child: const Text('Ver X-Burguer'),
),
ElevatedButton(
// Ou usando goNamed para o mesmo resultado
onPressed: () => context.goNamed(
AppRoutes.nomeProduto,
pathParameters: {'id': 'p001'},
),
child: const Text('Ver X-Burguer (por nome)'),
),
],
);
}
}
// Stubs das telas (para compilação do exemplo)
class TelaBase extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const TelaBase({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) => Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (i) => navigationShell.goBranch(i, initialLocation: i == navigationShell.currentIndex),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem(icon: Icon(Icons.receipt_long), label: 'Pedidos'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Perfil'),
],
),
);
}
class TelaInicio extends StatelessWidget { const TelaInicio({super.key}); @override Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Início'))); }
class TelaListaPedidos extends StatelessWidget { const TelaListaPedidos({super.key}); @override Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Pedidos'))); }
class TelaDetalhePedido extends StatelessWidget { final String idPedido; const TelaDetalhePedido({super.key, required this.idPedido}); @override Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Pedido $idPedido')), body: const Center(child: Text('Detalhe'))); }
class TelaPerfil extends StatelessWidget { const TelaPerfil({super.key}); @override Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Perfil'))); }
class TelaLogin extends StatelessWidget { const TelaLogin({super.key}); @override Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Login'))); }
class TelaDetalheProduto extends StatelessWidget { final String idProduto; const TelaDetalheProduto({super.key, required this.idProduto}); @override Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Produto $idProduto')), body: const Center(child: Text('Detalhe produto'))); }
class TelaNaoEncontrada extends StatelessWidget { const TelaNaoEncontrada({super.key}); @override Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Não encontrado'))); }import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
abstract class AppRoutes {
static const inicio = '/inicio';
static const catalogo = '/catalogo';
static const pedidos = '/pedidos';
static const perfil = '/perfil';
static const login = '/login';
static const nomeInicio = 'inicio';
static const nomeProduto = 'produto';
static const nomePedido = 'pedido';
static const nomeLogin = 'login';
static String detalheProduto(String id) => '/produto/$id';
static String detalhePedido(String id) => '/pedidos/$id';
}
class AppRouter {
static final roteador = GoRouter(
initialLocation: AppRoutes.inicio,
routes: [
StatefulShellRoute.indexedStack(
builder: (_, __, shell) => TelaBase(shell: shell),
branches: [
StatefulShellBranch(routes: [
GoRoute(
name: AppRoutes.nomeInicio,
path: AppRoutes.inicio,
builder: (_, __) => const TelaInicio()),
]),
StatefulShellBranch(routes: [
GoRoute(
path: AppRoutes.pedidos,
builder: (_, __) => const TelaListaPedidos(),
routes: [
GoRoute(
name: AppRoutes.nomePedido,
path: ':id',
builder: (_, s) =>
TelaDetalhePedido(idPedido: s.pathParameters['id']!)),
],
),
]),
StatefulShellBranch(routes: [
GoRoute(path: AppRoutes.perfil, builder: (_, __) => const TelaPerfil()),
]),
],
),
GoRoute(
name: AppRoutes.nomeLogin,
path: AppRoutes.login,
builder: (_, __) => const TelaLogin()),
GoRoute(
name: AppRoutes.nomeProduto,
path: '/produto/:id',
builder: (_, s) =>
TelaDetalheProduto(idProduto: s.pathParameters['id']!)),
],
errorBuilder: (context, _) => const TelaNaoEncontrada(),
);
}
void main() => runApp(MaterialApp.router(
title: 'Aplicativo de Pedidos',
theme: ThemeData(
useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)),
routerConfig: AppRouter.roteador,
));A Estrutura de Rotas do Projeto Integrador
Para fechar este módulo, é importante visualizar como a estrutura de navegação que você acabou de aprender se aplica ao Projeto Integrador do professor — o aplicativo de pedidos que serve como fio condutor da disciplina. A figura a seguir mostra o mapa de navegação completo da aplicação, ilustrando as rotas protegidas, as rotas públicas, o fluxo de autenticação e a hierarquia de sub-rotas.
flowchart TD
subgraph Público
L["/login<br/>TelaLogin"]
end
subgraph Autenticado["Autenticado (ShellRoute com BottomNavigationBar)"]
direction LR
I["/inicio<br/>TelaInicio"]
C["/catalogo<br/>TelaCatalogo"]
P["/pedidos<br/>TelaListaPedidos"]
PF["/perfil<br/>TelaPerfil"]
end
subgraph Detalhes["Sub-rotas (sem BottomNavBar visível no ShellRoute)"]
DP["/produto/:id<br/>TelaDetalheProduto"]
PP["/pedidos/:id<br/>TelaDetalhePedido"]
PA["/pedidos/:id/acompanhar<br/>TelaAcompanharPedido"]
CF["/confirmacao<br/>TelaConfirmacaoPedido"]
end
L -->|"login bem-sucedido<br/>context.go('/inicio')"| I
I --> C
I --> P
I --> PF
C -->|"context.push('/produto/:id')"| DP
P -->|"context.go('/pedidos/:id')"| PP
PP -->|"context.go('/pedidos/:id/acompanhar')"| PA
DP -->|"Adicionar → context.go('/confirmacao')"| CF
CF -->|"context.go('/inicio')"| I
PF -->|"Logout → context.go('/login')"| L
style L fill:#E53935,color:#fff
style Autenticado fill:#E8F5E9
style Detalhes fill:#E3F2FD
Este mapa mostra três características da navegação que são fundamentais para o Projeto Integrador. Primeiro, a separação clara entre rotas públicas (acessíveis sem autenticação) e rotas protegidas, controlada pelo redirect. Segundo, o StatefulShellRoute envolvendo as quatro abas principais da aplicação autenticada, garantindo que a BottomNavigationBar persista enquanto o usuário navega entre elas. Terceiro, as sub-rotas de detalhe que ficam fora do shell — quando o usuário navega para o detalhe de um produto ou para o acompanhamento de um pedido, a barra de navegação inferior desaparece, pois essas telas são contextuais e têm seus próprios botões de voltar.
Uma decisão arquitetural que você vai precisar tomar no seu Projeto Integrador é onde colocar as rotas de detalhe: dentro do StatefulShellRoute (mantendo a BottomNavigationBar visível mesmo no detalhe) ou fora do shell (ocultando a barra de navegação no detalhe). Nenhuma das duas é objetivamente certa ou errada — depende do design que o seu grupo escolheu. A abordagem mostrada acima, com os detalhes fora do shell, é a mais comum em aplicações de e-commerce e pedidos, pois cria um foco visual no conteúdo principal da tela de detalhe.
Erros Comuns e Como Evitá-los
Ao implementar a navegação com go_router, alguns erros aparecem com mais frequência do que outros. Conhecê-los antecipadamente vai poupar tempo de depuração.
O primeiro erro é criar o GoRouter dentro do método build de um widget. Cada vez que o widget pai reconstruir, uma nova instância do GoRouter será criada, o que redefine toda a pilha de navegação. O GoRouter deve ser criado fora de qualquer build — como um campo estático, uma variável de nível superior, ou dentro de uma classe de configuração como o AppRouter mostrado acima.
O segundo erro é misturar context.go e context.push de forma inconsistente. Use context.go para navegações que representam mudanças de estado permanentes (login, logout, conclusão de um fluxo) e context.push para navegações contextuais com retorno natural (abrir um detalhe, editar um campo). Misturar os dois sem intenção clara cria comportamentos de pilha inesperados.
O terceiro erro é esquecer o refreshListenable no GoRouter quando o redirect depende de estado externo. Se o estado de autenticação mudar mas o go_router não souber que precisa reavaliar o redirect, o usuário ficará na tela errada até a próxima navegação.
O quarto erro é não tratar a nulidade dos path parameters. O state.pathParameters['id'] retorna String?, não String. O operador ! é seguro somente quando a rota garante que o parâmetro sempre existirá — o que é o caso para path parameters definidos no padrão da rota. Para query parameters, que podem ou não estar presentes na URL, trate sempre a nulidade com ?? ou com uma verificação de null explícita.
O go_router oferece uma ferramenta de depuração muito útil: o parâmetro debugLogDiagnostics: true no construtor do GoRouter. Com ele ativado, o console do Flutter exibe informações detalhadas sobre cada mudança de rota, incluindo qual rota foi correspondida, quais parâmetros foram extraídos e se algum redirecionamento foi aplicado. Ative essa opção durante o desenvolvimento e desative antes do build de produção.
Consolidando o Aprendizado: Uma Visão do Percurso
Você chegou ao final do Módulo 05 com um conjunto sólido de ferramentas para conectar as telas do seu aplicativo. Partiu da metáfora fundamental da pilha de telas, entendeu como o Navigator 1.0 funciona e por que suas limitações motivaram o desenvolvimento do Navigator 2.0, e aprendeu a utilizar o go_router para construir uma estrutura de navegação declarativa, organizada e robusta.
Os conceitos que você dominou neste módulo — configuração do GoRouter, navegação com context.go e context.push, parâmetros de rota, sub-rotas, redirect para guards de autenticação, StatefulShellRoute para navegação em abas e deep linking — são a fundação sobre a qual todos os módulos seguintes vão construir. No Módulo 06, você vai implementar formulários e validação dentro das telas conectadas por essa estrutura. No Módulo 07, o Provider vai gerenciar o estado que o redirect usa para decidir se o usuário pode acessar cada rota. No Módulo 12, as notificações push vão usar deep linking para abrir telas específicas diretamente.
O Projeto Integrador, na sua aula prática desta semana, vai ganhar toda a sua estrutura de navegação. Isso significa criar o AppRouter, definir o StatefulShellRoute com as abas da aplicação, conectar o guard de autenticação e implementar as transições entre as telas principais. É um passo significativo na construção do produto que o grupo está desenvolvendo — e é uma das partes mais visíveis do progresso, pois o aplicativo deixará de ter uma tela única e ganhará a sensação de uma aplicação real, navegável.
Antes da aula prática, certifique-se de que o grupo chegou a um consenso sobre o mapa de navegação do aplicativo: quais são as rotas públicas, quais são as protegidas, quais abas compõem o shell autenticado e quais sub-rotas existem dentro de cada aba. Ter esse mapa desenhado em papel antes de escrever código acelera significativamente a implementação e evita retrabalho.