Flutter go_router: o guia essencial

Tempo de leitura: 7 minutes

Go_router é um pacote da comunidade flutter.dev para roteamento no Flutter que visa fornecer uma solução mais flexível e fácil de usar do que as opções de roteamento padrão fornecidas pelo Flutter. Pode ser útil se você quiser mais controle sobre como as rotas são definidas e gerenciadas em seu aplicativo. Ele também tem um bom suporte para web, o que é uma boa escolha para o seu aplicativo.

Você pode definir padrões de URL, navegar usando um URL, lidar com links diretos e vários outros cenários relacionados à navegação.

 

Características

O GoRouter possui vários recursos para simplificar a navegação:

  • Analisando o caminho e os parâmetros de consulta usando uma sintaxe de modelo
  • Exibição de várias telas para um destino (sub-rotas)
  • Suporte de redirecionamento — você pode redirecionar o usuário para uma URL diferente com base no estado do aplicativo, por exemplo, para um login quando o usuário não está autenticado
  • Suporte a navegação de guia aninhada com StatefulShellRoute
  • Suporte para aplicativos Material e Cupertino
  • Compatibilidade com versões anteriores com Navigator API

 

Iniciar

Para começar, adicione go_router ao seu pubspec.yaml. Neste artigo, usaremos ^7.1.1.

dependencies:
  go_router: ^7.1.1

 

Configuração de rota

Depois de fazer isso, vamos adicionar a configuração do GoRouter ao seu aplicativo:

import 'package:go_router/go_router.dart';

// GoRouter configuration
final _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      name: 'home', // Optional, add name to your routes. Allows you navigate by name instead of path
      path: '/',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      name: 'page2',
      path: '/page2',
      builder: (context, state) => Page2Screen(),
    ),
  ],
);

Em seguida, podemos usar o construtor MaterialApp.router ou CupertinoApp.router e definir o parâmetro routerConfig para seu objeto de configuração GoRouter:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

É isso 🙂 você está pronto para brincar com o go_router !!!

 

Parâmetros

Para especificar um parâmetro de caminho, prefixe um segmento de caminho com um caractere :, seguido por um nome exclusivo, por exemplo, :userId. Acessamos o valor do parâmetro pelo objeto GoRouterState fornecido ao callback do construtor:

GoRoute(
  path: '/fruits/:id',
  builder: (context, state) {
     final id = state.params['id'] // Get "id" param from URL
     return FruitsPage(id: id);
  },
),

Também podemos acessar o parâmetro de string de consulta usando GoRouterState. Por exemplo, um caminho de URL como /fruits?search=antonio pode ler o parâmetro de pesquisa:

GoRoute(
  path: '/fruits',
  builder: (context, state) {
    final search = state.queryParams['search'];
    return FruitsPage(search: search);
  },
),

 

Adicionando rotas secundárias

Uma rota correspondente pode resultar na exibição de mais de uma tela em um navegador. Isso é equivalente a chamar push(), onde uma nova tela é exibida acima da tela anterior e um botão Voltar no aplicativo no widget AppBar é fornecido.

Para fazer isso, adicionamos um child route e suas parent routers:

GoRoute(
  path: '/fruits',
  builder: (context, state) {
    return FruitsPage();
  },
  routes: <RouteBase>[ // Add child routes
    GoRoute(
      path: 'fruits-details', // NOTE: Don't need to specify "/" character for router’s parents
      builder: (context, state) {
        return FruitDetailsPage();
      },
    ),
  ],
)

Navegação entre telas

Há muitas maneiras de navegar entre destinos com go_router.

Para mudar para uma nova tela, chame context.go() com uma URL:

build(BuildContext context) {
  return TextButton(
    onPressed: () => context.go('/fruits/fruit-detail'),
  );
}

Também podemos navegar por nome em vez de URL, chame context.goNamed()

build(BuildContext context) {
  return TextButton(
    // remember to add "name" to your routes
    onPressed: () => context.goNamed('fruit-detail'),
  );
}

Para construir um URI com parâmetros de consulta, você pode usar a classe Uri:

context.go(
  Uri(
    path: '/fruit-detail',
    queryParameters: {'id': '10'},
   ).toString(),
);

Podemos abrir a tela atual via context.pop().

Nested Tab navigation

Alguns aplicativos exibem destinos em uma subseção da tela, por exemplo, um BottomNavigationBar que permanece na tela ao navegar entre as telas.

Configuramos o nested navigation usando StatefulShellRoute.

Essa classe StatefulShellRoute coloca sua sub-rota em um Navegador diferente do Navegador raiz. No entanto, essa classe de rota difere por criar navegadores separados para cada uma de suas ramificações nested (ou seja, árvores de navegação paralelas), tornando possível criar um aplicativo com navegação aninhada com estado.

Isso é conveniente, por exemplo, ao implementar uma interface do usuário com um BottomNavigationBar, com um estado de navegação persistente para cada guia.

Um StatefulShellRoute é criado especificando uma lista de itens StatefulShellBranch, cada um representando uma ramificação com estado separada na árvore de rota. StatefulShellBranch fornece as rotas raiz e a chave Navigator (GlobalKey) para a ramificação e um local inicial opcional.

Vamos ver como implementá-lo 🙂

Começamos criando nosso router, vamos adicionar StatefulShellRoute.indexedStack() em nossas rotas, essa classe será responsável por criar nossa navegação aninhada.

StatefulShellRoute.indexedStack() constrói um StatefulShellRoute que usa um IndexedStack para seus navegadores aninhados.

Esse construtor fornece uma implementação baseada em IndexedStack para o contêiner (navigatorContainerBuilder) usado para gerenciar os Widgets que representam os navegadores de ramificação.

// Create keys for `root` & `section` navigator avoiding unnecessary rebuilds
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _sectionNavigatorKey = GlobalKey<NavigatorState>();

final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/feed',
  routes: <RouteBase>[
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        // Return the widget that implements the custom shell (e.g a BottomNavigationBar).
        // The [StatefulNavigationShell] is passed to be able to navigate to other branches in a stateful way.
        return ScaffoldWithNavbar(navigationShell);
      },
      branches: [
        // The route branch for the 1º Tab
        StatefulShellBranch(
          navigatorKey: _sectionNavigatorKey,
          // Add this branch routes
          // each routes with its sub routes if available e.g feed/uuid/details
          routes: <RouteBase>[
            GoRoute(
              path: '/feed',
              builder: (context, state) => const FeedPage(),
              routes: <RouteBase>[
                GoRoute(
                  path: 'detail',
                  builder: (context, state) => const FeedDetailsPage(),
                )
              ],
            ),
          ],
        ),

        // The route branch for 2º Tab
        StatefulShellBranch(routes: <RouteBase>[
          // Add this branch routes
          // each routes with its sub routes if available e.g shope/uuid/details
          GoRoute(
            path: '/shope',
            builder: (context, state) => const ShopePage(),
          ),
        ])
      ],
    ),
  ],
);

Adicionamos StatefulShellRoute.indexedStack() à nossa rota, ele é responsável por criar nossas branches e retornar um shell customizado (neste caso um BottomNavigationBar).

  1. No builder: (context, state, navigationShell) retornamos nosso shell personalizado, basicamente um Scaffold com um BottomNavigationBar, lembre-se de passar o navigationShell para esta página, pois usaremos isso para navegar para outras ramificações (por exemplo, Home ==> Shope)
  2. Nos branches:[] damos uma lista de StatefulShellBranch (nossos ramos). Passamos nossa _sectionNavigatorKey criada anteriormente para a propriedade navigatorKey, mas apenas para a primeira ramificação, uma chave padrão será usada para outras ramificações. Também fornecemos uma lista de RouteBase (as rotas suportadas para essa ramificação)

Como você pode ver, nosso construtor retorna nosso shell personalizado que contém nosso BottomNavigationBar, então vamos criá-lo 👇🏿

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class ScaffoldWithNavbar extends StatelessWidget {
  const ScaffoldWithNavbar(this.navigationShell, {super.key});

  /// The navigation shell and container for the branch Navigators.
  final StatefulNavigationShell navigationShell;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shope'),
        ],
        onTap: _onTap,
      ),
    );
  }

  void _onTap(index) {
    navigationShell.goBranch(
      index,
      // Um padrão comum ao usar as barras de navegação inferiores é dar suporte à 
      // navegação para o local inicial ao tocar no item que 
      // já está ativo. Este exemplo demonstra como oferecer suporte a esse comportamento, 
      // usando o parâmetro initialLocation de goBranch.
      initialLocation: index == navigationShell.currentIndex,
    );
  }
}

Basicamente retornamos um Scaffold com BottomNavigationBar, o body vai ser um navigationShell que pegamos do nosso roteador.

Há também um _onTap(index) , aqui usamos navigationShell.goBranch(index) assim podemos mudar entre as ramificações.

E pronto, você está pronto para implementar isso em seus projetos 🥳🎉

Para um exemplo completo, verifique meu repositório abaixo 👇🏿

 

Guardas

Para proteger routes específicos, por exemplo de usuários não autenticados, o redirect global pode ser configurado via GoRouter. Um exemplo mais comum seria o redirecionamento configurado que protege qualquer rota que não seja /login e redireciona para /login se o usuário não estiver autenticado

Um redirect é um retorno de chamada do tipo GoRouterRedirect. Para alterar o local de entrada com base em algum estado do aplicativo, adicione um retorno de chamada ao construtor GoRouter ou GoRoute:

GoRouter(
  redirect: (BuildContext context, GoRouterState state) {
    final isAuthenticated = // your logic to check if user is authenticated
    if (!isAuthenticated) {
      return '/login';
    } else {
      return null; // return "null" to display the intended route without redirecting
     }
   },
  ...
  • Você pode definir o redirecionamento no construtor GoRouter. Chamado antes de qualquer evento de navegação.
  • Defina o redirecionamento no construtor GoRoute. Chamado quando um evento de navegação está prestes a exibir a route.

Você pode especificar um redirectLimit para configurar o número máximo de redirecionamentos que devem ocorrer em seu aplicativo. Por padrão, esse valor é definido como 5. O GoRouter exibirá a tela de erro se esse limite de redirecionamento for excedido

 

Animações de transição

O GoRouter permite que você personalize a animação de transição para cada GoRoute. Para configurar uma animação de transição personalizada, forneça um parâmetro pageBuilder ao construtor GoRoute:

GoRoute(
  path: '/fruit-details',
  pageBuilder: (context, state) {
    return CustomTransitionPage(
      key: state.pageKey,
      child: FruitDetailsScreen(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        // Change the opacity of the screen using a Curve based on the the animation's value
        return FadeTransition(
          opacity: CurveTween(curve: Curves.easeInOutCirc).animate(animation),
          child: child,
        );
      },
    );
  },
),

Para obter um exemplo completo, consulte a amostra de animações de transição.

 

Tratamento de erros (página 404)

Por padrão, o go_router vem com telas de erro padrão para MaterialApp e CupertinoApp, bem como uma tela de erro padrão caso nenhuma seja usada. Você também pode substituir a tela de erro padrão usando o parâmetro errorBuilder:

GoRouter(
  /* ... */
  errorBuilder: (context, state) => ErrorPage(state.error),
);

 

Rotas de tipo seguro

Em vez de usar strings de URL (context.go(“/auth”)) para navegar, go_router oferece suporte a rotas de tipo seguro usando o pacote go_router_builder.

Para começar, adicione go_router_builder, build_runner e build_verify à seção dev_dependencies de seu pubspec.yaml:

dev_dependencies:
  go_router_builder: ^2.0.2
  build_runner: ^2.4.4
  build_verify: ^3.1.0

 

Definindo uma route

Em seguida, defina cada rota como uma classe estendendo GoRouteData e substituindo o método build.

class HomeRoute extends GoRouteData {
  const HomeRoute();
  
  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

 

Route tree

A route tree é definida como um atributo em cada uma das routes de nível superior:

import 'package:go_router/go_router.dart';

part 'go_router.g.dart'; // name of generated file

// Define how your route tree (path and sub-routes)
@TypedGoRoute<HomeScreenRoute>(
    path: '/home',
    routes: [ // Add sub-routes
      TypedGoRoute<SongRoute>(
        path: 'song/:id',
      )
    ]
)

// Create your route screen that extends "GoRouteData" and @override "build"
// method that return the screen for this route
@immutable
class HomeScreenRoute extends GoRouteData {
  @override
  Widget build(BuildContext context) {
    return const HomeScreen();
  }
}

@immutable
class SongRoute extends GoRouteData {
  final int id;
  const SongRoute({required this.id});

  @override
  Widget build(BuildContext context) {
    return SongScreen(songId: id.toString());
  }
}

Para construir os arquivos gerados, use o comando build_runner:

flutter pub global activate build_runner // Optional, if you already have build_runner activated so you can skip this step
flutter pub run build_runner build

Para navegar, construa um objeto GoRouteData com os parâmetros necessários e chame go():

TextButton(
  onPressed: () {
    const SongRoute(id: 2).go(context);
  },
  child: const Text('Go to song 2'),
),

 

Antes de você ir !!!

Ainda há um bom recurso com go_router, você pode adicionar um NavigatorObserver ao nosso GoRouter para observar o comportamento de um Navigator, ouvir sempre que uma rota foi push, pop ou replace. Para isso, vamos criar uma classe que estenda NavigatorObserver :

class MyNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    log('did push route');
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    log('did pop route');
  }
}

Agora vamos adicionar MyNavigatorObserver ao nosso GoRouter

GoRouter(
  ...
  observers: [ // Add your navigator observers
    MyNavigatorObserver(),
  ],
...
)

Sempre que esses eventos forem acionados, seu navegador será notificado.

 

Encontre aqui o exemplo de projeto 👇🏿👇🏿