StatefulShellRoute e navegação sem contexto com go_router e get_it

Kaue Murakami
8 min readJan 22, 2025

--

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

--

--

Kaue Murakami
Kaue Murakami

Written by Kaue Murakami

Ninja in Dart - Flutter and NodeJS - JavaScript

No responses yet