StatefulShellRoute e navegação sem contexto com go_router e get_it
O StatefulShellRoute
no go_router é uma solução poderosa para gerenciar o estado de rotas que compartilham uma mesma interface ou estrutura, como em aplicativos com navegação por abas ou layouts persistentes. Ele permite criar uma navegação onde diferentes sub-rotas podem coexistir mantendo seu estado específico, como scrolls ou interações, mesmo quando o usuário alterna entre elas.
Essa abordagem é útil, por exemplo, em aplicativos que possuem uma barra de navegação inferior, onde mudar de aba não reseta o estado das páginas visitadas anteriormente. O StatefulShellRoute
se destaca por facilitar a experiência do usuário ao preservar o contexto de cada rota, enquanto garante uma navegação fluida e consistente.
Neste artigo
Vamos ver como implementar essa navegação interna coma a versão mais recente do go_router e usar o get_it para injetar e recuperar nossa intância de GoRouter do nosso delegate para podermos navegar a partir dele, independente do contexto.
Começando
Crie um novo projeto flutter:
$ flutter create stful_shell_route
Criando a estrutura
Crie a seguinda herarquia de pastas em lib/
:
lib/
models/
views/
utils/
functions/
extensions/
routes/
delegate/
...
transitions/
...
main.dart
Agora vamos adicionar nossas dependências go_router e get_it no pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
go_router: ^14.6.3
get_it: ^8.0.3
No momento do desenvolvimento deste artigo, são as versões mais recentes.
Mãos à massa!!!
Vamos começar nossas modificações no arquivo main.dart
, removendo a classe MyHomePage
por completo, mantendo apenas a classe MyApp
que possui o widget MaterialApp
:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'StfulShellRoute',
theme: ThemeData(
useMaterial3: true,
),
...
);
}
}
Agora, para adicionarmos a navegação 2.0, que iremos implementar com go_router vamos fazer uma pequena modificação, ainda na main.dart
:
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'StfulShellRoute',
theme: ThemeData(
useMaterial3: true,
),
routerConfig: ,
);
}
Pronto, agora só precisamos criar nosso routerConfig:
Router Config
Nosso routerConfig:
é responsável por fornecer nossos Delegates, InformationParser e InformationProvider para a configuração do router.
Criando Delegate
Na pasta criada routes/delegate/
crie um arquivo chamado delegate.dart
, esse arquivo será responsável por armazenar nossas rotas, subrotas e suas configurações, e estará, inicialmente, assim:
class MyGoRouterDelegate {
static final _rootNavigatorKey = GlobalKey<NavigatorState>();
GoRouter get router => _router;
final GoRouter _router = GoRouter(
debugLogDiagnostics: true,
navigatorKey: _rootNavigatorKey,
initialLocation: AppRoutes.home,
routes: <RouteBase>[
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/',
redirect: (context, state) {
print(state.fullPath);
return AppRoutes.home;
},
),
...
],
);
}
Criando arquivos necessários
Você pode ver que possuímos algumas classes ainda não criadas até aqui, vamos cria-las agora:
AppRoutes
É uma classe abstrata, pois não queremos instâncias dela, seus atributos são static const
para para armazenar o nome/path da rota, caso prefira, pode adicionar a string diretamente na sua rota, ao invés de usar essa classe, a vantagem é que, caso essa rota mude algum dia, você poderá, de um só lugar, alterar seu valor, mas você também pode usar os valores diretamente nas suas rotas como ‘/home’ ao invés de AppRoutes.home
.
Em seu diretório routes/
crie um arquivo chamado routes.dart
com o seguinte conteúdo:
abstract class AppRoutes {
static const home = '/home';
static const profile = '/profile';
static const settings = '/settings';
static const product = ':id';
static const productName = 'products';
}
Pederíamos já adicionar nossa classe delegate agora no MaterialApp.router(routerConfig: MyGoRouterDelegate.router
,pra isso, poderíamos apenas remover o _
de _router
e adicionar a palavra reservada static
antes de router, mas como vamos usar o get_it para injetar nossas dependências, vamos manter como está e fazer a configuração do get_it antes de adicionar nosso router.
setupLocator
Implementando get_it e injetando a dependência MyGoRouterDelegate
.
No diretório utils/functions
crie uma arquivo chamado setup_locator.dart
, o mesmo será responsável por criar nossa instância de get_it, inicializar e injetar nossa dependência.
GetIt getIt = GetIt.instance;
void setupLocator() {
getIt.registerLazySingleton<MyGoRouterDelegate>(() => MyGoRouterDelegate());
}
Aqui criamos uma instância de GetIt e temos uma função que será responsável por registrar nosso delegate como um singleton, deixando assim, disponível pra toda aplicação através da instância getIt
Finalizando arquivo main
void main() {
setupLocator();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'StfulShellRoute',
theme: ThemeData(
useMaterial3: true,
),
routerConfig: getIt<MyGoRouterDelegate>().router,
);
}
}
Agora, após injetar nossa dependência com setupLocator()
, podemos recuperar nosso router
de MyGoRouterDelegate
através de getIt
instância.
Criando StatefulShellRoute
Vamos agora criar nossa hierarquia de rotas no nosso delegate, implementando StatefulShellRoute
para uma navegação aninhada:
class MyGoRouterDelegate {
static final _rootNavigatorKey = GlobalKey<NavigatorState>();
GoRouter get router => _router;
final GoRouter _router = GoRouter(
debugLogDiagnostics: true,
navigatorKey: _rootNavigatorKey,
initialLocation: AppRoutes.home,
routes: <RouteBase>[
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/',
redirect: (context, state) {
print(state.fullPath);
return AppRoutes.home;
},
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return DashboardView(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.home,
name: AppRoutes.home.routeName,
pageBuilder: (context, state) {
print(state.fullPath);
return CustomFadeTransition(
child: const HomeView(),
);
},
routes: [
GoRoute(
path: AppRoutes.product,
name: AppRoutes.productName,
pageBuilder: (context, state) {
print('${state.fullPath!.replaceFirst(':id', state.pathParameters['id']!)}');
return CustomFadeTransition(
child: ProductView(
// id: int.parse(state.pathParameters['id']!),
//pode passar por parametro para a view/controller
//, no meu caso usei minha instancia do router com
// getIt para recuperar o state da rota juntamente
// com os parametros passados, graças a ele podemos
// navegar sem context, baseado no nosso router.
),
);
},
),
],
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.profile,
name: AppRoutes.profile.routeName,
pageBuilder: (context, state) {
print(state.fullPath);
return CustomFadeTransition(
child: const ProfileView(),
);
}),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.settings,
name: AppRoutes.settings.routeName,
pageBuilder: (context, state) {
print(state.fullPath);
return CustomFadeTransition(
child: const SettingsView(),
);
},
),
],
),
],
),
],
);
}
StatefulShellRoute
O StatefulShellRoute
como dito no início deste artigo, nos permite criar uma navegação onde diferentes rotas compartilham um mesmo layout persistente (como uma barra de navegação inferior ou um drawer), no nosso caso, a DashboardView
que possui um NavigationBar
, enquanto cada rota mantém seu estado independente. Ele também organiza a navegação em branchs (ramificações), onde cada branch representa um conjunto de rotas que podem ser navegadas de forma independente, podendo também possuir sub-rotas.
Observe que nossa rota inicial é AppRoutes.home
, que é redirecionada a partir da rota raiz /
, por sua vez, a rota home
está configurada dentro de um StatefulShellRoute.indexedStack
, como uma StatefulShellBranch
ao invés de um GoRouter
diretamente, isso, como explicado acima, porquê o StatefulShellRoute
organiza sua navegação em branchs
, que por sua vez, possuem nosso GoRouter
. Imagine cada branch como um item do seu NavigationBar
, e cada branch, pode ter ainda rotas internas a ela, como no nosso exemplo, a rota home
possui uma rota filha product
que tem como path esperado um id
.
Todas as rotas do nosso StatefulShellRoute
são renderizadas dentro da page DashboardView
, vamos ver como isso funciona.
DashboardView
No diretório que você criou views/
adicione outro diretório chamado dashboard
, vamos usar um diretório ao invés de um arquivo direto sempre que essa view tiver widgets componentizados.
Com o diretório criado, crie dentro dele um outro diretório chamado widgets/
dentro dele crie o arquivo bottom_navigation.dart
.
Agora na raiz dashboard/
crie o arquivo dashboard.dart
.
Implementando
bottom_navigation.dart
import 'package:flutter/material.dart';
class BottomNavigationWidget extends StatelessWidget {
const BottomNavigationWidget({
super.key,
required this.currentIndex,
required this.onTap,
});
final int currentIndex;
final Function(int) onTap;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: currentIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.verified_user),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
onTap: onTap,
);
}
}
dashboard.dart
class DashboardView extends StatelessWidget {
const DashboardView({super.key, required this.navigationShell});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationWidget(currentIndex: navigationShell!.currentIndex, onTap: _switchBranch),
body: navigationShell,
);
}
_switchBranch(int index) {
navigationShell.goBranch(index);
initialLocation:
index == navigationShell.currentIndex;
}
}
Agora podemos ver o parâmetro navigationShell
que estava sendo passado do nosso:
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return DashboardView(navigationShell: navigationShell);
},
branches: [...]
Ele é responsável por exibir o widget da rota atual, ou seja, ele controla as branchs que serão exibidas de acordo com o index
atual.
Sei que você ainda não consegue rodar essa aplicação, vamos então criar o restante dos arquivos necessários pra isso, e você poderá navegar, depois irei detalhar o funcionamento.
Criando arquivos necessários
Para o artigo não ficar muito grande, irei apenas apontar os arquivos que devem ser criados pra um link do repositório do projeto pra cada arquivo específico para que vocês possam copiar, já que são páginas “vazias”, especificarei o que for importante.
Você pôde reparar que no name
de cada GoRoute
possui uma extensão, isso porquê criei um extensão para remover o /
do path
que é definido em AppRoutes
, essa extensão remove, caso exista o /
da string para usarmos como name, isso não é obrigatório, fiz apenas para simplificar.
Em utils/extensions/
crie um arquivo chamado route_name.dart
com o seguinte conteúdo
extension RouteName on String {
String get routeName => contains('/') ? replaceFirst('/', '') : this;
}
Transition
Também temos um widget
sobre nossas rotas, aqui estou utilizando apenas o CustomFadeTransition
, mas no repositório deixei disponíveis mais tipos de transição de página.
Em routes/
crie um diretório chamado transitions/
, dentro deste diretório crie um arquivo chamado fade_transition.dart
com o seguinte conteúdo, ainda em transitions/
crie um arquivo chamado slide_from.dart
, com o seguinte conteúdo.
Criando views/pages
Vamos agora criar as páginas/views que aparecem no nosso delegate
, sendo elas padrões exceto a home
, que iremos entrar em detalhes.
Em views/
crie o arquivo profile.dart
com o seguinte conteúdo
Em views/
crie o arquivo settings.dart
com o seguinte conteúdo
Em views/
crie um diretório chamado home/
, crie um arquivo chamado home.dart
com o seguinte conteúdo, agora no diretóriohome/
crie um outro diretório chamado widgets/
, dentro dele um arquivo chamado product_item.dart
com o seguinte conteúdo
ProductModel
No diretório models/
crie um arquivo chamado product_model.dart
com o seguinte conteúdo
Rodando o projeto
Agora você tem todos os arquivos necessários para rodar o projeto, use flutter run
e teste o projeto.
Pronto, você já pode navegar com StatefulShellRoute
.
Vamos agora falar sobre o que acontece quando selecionamos um item da lista de produtos gerada.
Repare que a rota AppRoutes.product
é = :id , isso significa que esperamos uma parâmetro id
para construir a url dessa rota, observe nossa rota:
routes: [
GoRoute(
path: AppRoutes.product, // = :id
name: AppRoutes.productName,
pageBuilder: (context, state) {
print('${state.fullPath!.replaceFirst(':id',
state.pathParameters['id']!)}');
return CustomFadeTransition(
child: ProductView(
//poderia ser por param
//id: state.pathParameters['id']
),
);
},
),
],
Repare em como navegamos para a rota do product
em product_item.dart
return InkWell(
onTap: () => getIt<MyGoRouterDelegate>().router.pushNamed(
AppRoutes.productName,
pathParameters: {
'id': productModel.id.toString(),
},
),
....
Usando nossa instância de getIt
podemos recuperar nosso router
e navegar através dele, sem utilização de contexto.
Também estamos passando um parâmetro para mostrar a composição das rotas, repare no print que será mostrado no seu terminal, ele mostra o fullPath
, da rota atual completa.
E podemos recuperar esse parâmetro de duas formas:
— Passando por parâmetro pra view ou seu provider.
— Recuperando com o router atual
No exemplo estou recuperando com o GoRouter atual sem contexto, mas deixei comentado caso queira passar via parâmetro.
Então no nosso caso estamos recuperando o parâmetro na views/product.dart
dessa forma:
final int id = int.parse(
getIt<MyGoRouterDelegate>().router.state!.pathParameters['id']!
);
E veja que nossa rota atual é /home/<id>
.
Com isso chegamos ao fim
Espero ter ajudado vocês neste artigo, qualquer comentários e contribuições são bem-vindos.
Repositório do projeto https://github.com/kauemurakami/go_router_stful_shell_route