Flutter go_router: o guia essencial
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.
Conteudo
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
).
- 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)
- Nos
branches:[]
damos uma lista deStatefulShellBranch
(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 👇🏿👇🏿