Flutter: Navegação e Roteamento
Rotas são o mapa de um app — sem elas, os usuários são como motoristas sem GPS, incapazes de ir a qualquer lugar.
📋 Pré-requisitos: Você precisa dominar o seguinte primeiro
- Aula 6: Material Design e Widgets Comuns
- Aula 7: Prática da Fase 1 — ShopApp Starter
1. O Que Você Vai Aprender
- Navigator 1.0: push/pop/pushReplacement e gerenciamento da pilha de rotas
- Rotas nomeadas: tabela de rotas / onGenerateRoute / passagem de parâmetros
- GoRouter (Navigator 2.0): roteamento declarativo, ShellRoute aninhado, redirecionamentos
- Deep linking e estratégia de URL
- Sistema de roteamento GoRouter do ShopApp: /home /product/:id /cart /checkout
2. Uma História Real de Páginas Perdidas
(1) O Problema: Caos na Pilha de Rotas
O ShopApp do Bob tem 10 páginas, todas navegadas via Navigator.push. Um usuário vai de Início → Busca → Produto → Carrinho → Checkout, mas ao pressionar voltar retorna à página de Busca em vez de Início. A pilha de rotas tem 5 páginas, exigindo 4 pressionadas de voltar para chegar ao Início. Pior ainda, a URL Web não muda, e ao atualizar todo o estado é perdido.
(2) A Solução com GoRouter
O GoRouter usa roteamento declarativo onde URLs e páginas mapeiam automaticamente, e a pilha de rotas é determinada pela configuração em vez da ordem de push.
⚙️ Instalar dependência:
flutter pub add go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Declarative routing: URL ↔ Page mapping
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(path: '/product/:id', builder: (_, state) => DetailPage(id: state.pathParameters['id'])),
GoRoute(path: '/cart', builder: (_, __) => const CartPage()),
],
);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Benefício: Navegação Orientada por URL + Lógica de Voltar Automática
Após usar o GoRouter, a URL Web atualiza com as mudanças de página e atualizações preservam o estado; o botão voltar do Android segue a pilha de rotas automaticamente, e após o checkout, go('/') retorna diretamente ao Início.
3. Noções Básicas do Navigator 1.0
(1) Operações da Pilha de Rotas
| Método | Efeito | Mudança na Pilha |
|---|---|---|
push |
Empurra uma nova página | [A] → [A, B] |
pop |
Remove a página atual | [A, B] → [A] |
pushReplacement |
Substitui a página atual | [A, B] → [A, C] |
pushAndRemoveUntil |
Empurra e limpa até uma página alvo | [A,B,C] → [A, D] |
popUntil |
Remove até uma página alvo | [A,B,C] → [A] |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Operações básicas de navegação
import 'package:flutter/material.dart';
// Simplified class definitions
class Product {
const Product();
}
class DetailPage extends StatelessWidget {
final Product product;
const DetailPage({super.key, required this.product});
@override
Widget build(BuildContext context) => const Scaffold();
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) => const Scaffold();
}
// Push: navigate to detail page
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage(product: product)),
);
// Pop: go back with result
Navigator.pop(context, result);
// Push replacement: login → home (no back to login)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
// Push and remove until: checkout → home (clear all intermediate)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
(route) => route.isFirst,
);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
4. Rotas Nomeadas
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração de rotas nomeadas
import 'package:flutter/material.dart';
// Simplified page class definitions
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({super.key, required this.id});
@override Widget build(BuildContext context) => const Scaffold();
}
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/cart': (context) => const CartPage(),
'/checkout': (context) => const CheckoutPage(),
},
onGenerateRoute: (settings) {
// Handle dynamic routes like /product/123
final uri = Uri.parse(settings.name!);
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'product') {
final id = int.parse(uri.pathSegments[1]);
return MaterialPageRoute(builder: (_) => DetailPage(id: id));
}
return null;
},
)
// Navigate with named route
Navigator.pushNamed(context, '/product/42');
Navigator.pushNamed(context, '/cart', arguments: {'source': 'detail'});
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
| Abordagem | Prós | Contras |
|---|---|---|
| Push direto | Simples, intuitivo, com segurança de tipos | Rotas dispersas, difícil de manter |
| Rotas nomeadas | Gerenciamento centralizado, passagem de parâmetros | Parâmetros sem segurança de tipos |
| GoRouter | Declarativo, amigável para Web, deep linking | Requer dependência adicional |
5. Roteamento Declarativo com GoRouter
graph TD
GR[GoRouter] --> R1["/ → HomePage"]
GR --> R2["/product/:id → DetailPage"]
GR --> R3["/cart → CartPage"]
GR --> R4["/checkout → CheckoutPage"]
R1 --> |DeepLink| R2
R2 --> |addToCart| R3
R3 --> |checkout| R4
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(1) Conceitos Centrais do GoRouter
| Conceito | Descrição | Exemplo |
|---|---|---|
GoRoute |
Declaração de rota | GoRoute(path: '/cart') |
pathParameters |
Parâmetros de caminho | /product/:id → {'id': '42'} |
queryParams |
Parâmetros de consulta | ?sort=price → {'sort': 'price'} |
redirect |
Lógica de redirecionamento | Não logado → /login |
ShellRoute |
Layout aninhado | Sub-rotas compartilhando BottomNav |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Configuração GoRouter do ShopApp
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Simplified class definitions
class AuthService {
static bool isLoggedIn = false;
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int productId;
const DetailPage({super.key, required this.productId});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
final router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/product/:id',
name: 'product',
builder: (context, state) {
final id = int.parse(state.pathParameters['id']!);
return DetailPage(productId: id);
},
),
GoRoute(
path: '/cart',
name: 'cart',
builder: (context, state) => const CartPage(),
),
GoRoute(
path: '/checkout',
name: 'checkout',
builder: (context, state) => const CheckoutPage(),
),
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
],
);
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(2) Layout Aninhado com ShellRoute
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: ShellRoute com BottomNav
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// Simplified page class definitions
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CategoriesPage extends StatelessWidget {
const CategoriesPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class CartPage extends StatelessWidget {
const CartPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({super.key, required this.id});
@override Widget build(BuildContext context) => const Scaffold();
}
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override Widget build(BuildContext context) => const Scaffold();
}
final router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(path: '/categories', builder: (_, __) => const CategoriesPage()),
GoRoute(path: '/cart', builder: (_, __) => const CartPage()),
GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
],
),
GoRoute(path: '/product/:id', builder: (_, state) => DetailPage(
id: int.parse(state.pathParameters['id']!)),
),
GoRoute(path: '/checkout', builder: (_, __) => const CheckoutPage()),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex(context),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
NavigationDestination(icon: Icon(Icons.shopping_cart), label: 'Cart'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
onDestinationSelected: (index) => _onItemTapped(index, context),
),
);
}
int _selectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/categories')) return 1;
if (location.startsWith('/cart')) return 2;
if (location.startsWith('/profile')) return 3;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0: context.go('/');
case 1: context.go('/categories');
case 2: context.go('/cart');
case 3: context.go('/profile');
}
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Comparação de Métodos de Navegação
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: go vs push vs replace
import 'package:go_router/go_router.dart';
// go: replace entire stack (Web: URL changes)
context.go('/cart'); // Stack becomes [/cart]
// push: add to stack (Web: URL changes)
context.push('/product/42'); // Stack becomes [/, /product/42]
// replace: replace current route
context.replace('/checkout'); // Stack becomes [/, /checkout]
// go with named route
context.goNamed('product', pathParameters: {'id': '42'});
// go with query parameters
context.go('/products', queryParams: {'sort': 'price', 'order': 'desc'});
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
| Método | Operação na Pilha | Atualiza URL | Caso de Uso |
|---|---|---|---|
go |
Substitui toda a pilha | Sim | Navegação primária (alternância de abas) |
push |
Empurra nova rota | Sim | Navegação para página de detalhes |
replace |
Substitui a rota atual | Sim | Login → Início |
7. Exemplo Completo: Sistema de Roteamento do ShopApp
⚙️ Instalar dependência:
flutter pub add go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(ShopApp(router: router));
final router = GoRouter(
initialLocation: '/',
routes: [
ShellRoute(
builder: (context, state, child) => MainScaffold(child: child),
routes: [
GoRoute(path: '/', name: 'home', builder: (_, __) => const HomePage()),
GoRoute(path: '/categories', builder: (_, __) => const CategoriesPage()),
GoRoute(path: '/cart', name: 'cart', builder: (_, __) => const CartPage()),
GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
],
),
GoRoute(
path: '/product/:id',
name: 'product',
builder: (_, state) => DetailPage(id: int.parse(state.pathParameters['id']!)),
),
GoRoute(
path: '/checkout',
name: 'checkout',
builder: (_, __) => const CheckoutPage(),
),
],
errorBuilder: (_, state) => Scaffold(
body: Center(child: Text('Page not found: ${state.error}')),
),
);
class ShopApp extends StatelessWidget {
final GoRouter router;
const ShopApp({super.key, required this.router});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'ShopApp',
routerConfig: router,
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
);
}
}
class MainScaffold extends StatelessWidget {
final Widget child;
const MainScaffold({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _calcIndex(context),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
NavigationDestination(icon: Icon(Icons.shopping_cart), label: 'Cart'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
onDestinationSelected: (i) {
final paths = ['/', '/categories', '/cart', '/profile'];
context.go(paths[i]);
},
),
);
}
int _calcIndex(BuildContext context) {
final loc = GoRouterState.of(context).matchedLocation;
if (loc.startsWith('/categories')) return 1;
if (loc.startsWith('/cart')) return 2;
if (loc.startsWith('/profile')) return 3;
return 0;
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: GoRouter e Navigator 1.0 podem ser misturados? R: A mistura não é recomendada. O GoRouter gerencia o Navigator internamente, e misturar causa inconsistências na pilha de rotas.
P: Como ocultar o BottomNav para sub-rotas em um ShellRoute? R: Coloque rotas que não precisam de BottomNav fora do ShellRoute, como as páginas de detalhes e checkout.
P: Como o GoRouter implementa guardas de rota (autenticação)? R: Use o callback
redirect: verifique o status de login, redirecione para/loginse não estiver logado.
P: Estado da página Web perdido ao atualizar? R: O GoRouter suporta restauração de estado por URL por padrão. Certifique-se de usar
MaterialApp.routerem vez deMaterialApp.
P: Qual a diferença entre push e go? R: push empilha uma nova rota no topo; go substitui toda a pilha. Use go para navegação por abas e push para navegação de página de detalhes.
P: Como configurar deep linking no iOS? R: Você precisa configurar Associated Domains e um arquivo
apple-app-site-association. No Android, configure Asset Links e intent-filter.
📖 Resumo
- Navigator 1.0 gerencia a pilha de rotas com push/pop, adequado para cenários simples
- Rotas nomeadas centralizam o gerenciamento de caminhos, mas parâmetros não têm segurança de tipos
- Roteamento declarativo GoRouter: URL ↔ página com mapeamento automático, amigável para Web
- ShellRoute implementa layout compartilhado (BottomNav) com sub-rotas aninhadas
- redirect implementa guardas de rota; go/push/replace controlam operações da pilha
📝 Exercícios
- Básico (⭐): Use GoRouter para configurar 3 rotas (Início/Carrinho/Perfil) com alternância de navegação inferior.
- Intermediário (⭐⭐): Adicione uma rota de detalhes
/product/:id, navegue a partir dos cartões de produto na página inicial, e suporte passagem de parâmetros de caminho. - Desafio (⭐⭐⭐): Implemente uma guarda de rota completa: redirecione usuários não autenticados que acessam
/profilepara/login, e redirecione automaticamente de volta após o login.