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

1. O Que Você Vai Aprender


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

DART
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()),
  ],
);
TEXT
> 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

TEXT
> 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

DART
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,
);
TEXT
> 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

TEXT
> 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

DART
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'});
TEXT
> 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

100%
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
TEXT
> 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

TEXT
> 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

DART
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(),
    ),
  ],
);
TEXT
> 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

TEXT
> 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

DART
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');
    }
  }
}
TEXT
> 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

TEXT
> 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

DART
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'});
TEXT
> 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

DART
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;
  }
}
TEXT
> 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 /login se 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.router em vez de MaterialApp.

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


📝 Exercícios

  1. Básico (⭐): Use GoRouter para configurar 3 rotas (Início/Carrinho/Perfil) com alternância de navegação inferior.
  2. 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.
  3. Desafio (⭐⭐⭐): Implemente uma guarda de rota completa: redirecione usuários não autenticados que acessam /profile para /login, e redirecione automaticamente de volta após o login.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%