Привет, Хабр! Меня зовут Юрий Петров, я автор ютуб-канала «Мобильный разработчик» и Flutter Team Lead в Friflex. Мы разрабатываем мобильные приложения для бизнеса и специализируемся на Flutter.
В этом руководстве я хочу рассказать про пакет go_router. Он помогает управлять навигацией во Flutter. Команда разработки Flutter поддерживает данный пакет. Это позволяет надеяться, что в дальнейшем пакет продолжит развиваться.
Рассказывать буду на примере простого проекта — Todo (заметки). Я понимаю, что таких проектов на Хабре очень много. Но, по-моему, лучшего примера не найти.

Создаем проект и подключаем библиотеку
Немного подумав, я решил, что для нашего проекта необходимо как минимум шесть экранов.
Экран аутентификации пользователя
AuthScreen
. Будем использовать фейковую аутентификацию.Экран со списком заметок —
NotesScreen
.Экран с детальной информацией заметок —
DetailNoteScreen
.Экран с любимыми заметками —
FavoriteNotesScreen
.Экран для создания заметок —
CreateNoteScreen
.Экран для управления профилем —
ProfileScreen
.Экран для размещения рутового экрана с нижней навигационной панелью —
RootScreen
.
Для удобства создаем четыре папки в папке features. В каждой из них — экраны. В каждом файле создаем одноименный StatelessWidget.

Реализация нижней навигационной панели - BottomNavigationBar
Создаем папку routing, в которой будем хранить карту маршрутов. Для создания нижней навигационной панели будем использовать маршрут StatefulShellRoute. Этот маршрут отображает пользовательский интерфейс с отдельными навигаторами для всех его под маршрутов. Он работает как ShellRoute, но с одним исключением. Он создает отдельные навигаторы для каждой из своих вложенных ветвей, то есть, параллельных деревьев навигации.
С StatefulShellRoute
мы можем разрабатывать приложения с сохранением состояния вложенной навигации. Это удобно, например, при создании интерфейса с BottomNavigationBar
, где для каждой вкладки сохраняется свое состояние навигации.
Чтобы создать StatefulShellRoute
, указываем список элементов StatefulShellBranch
. Каждый из них представляет отдельную ветку маршрута с сохранением состояния.
StatefulShellBranch
предоставляет корневые маршруты и ключ навигатора (GlobalKey
) для ветки, а также начальное расположение. Как и в случае с ShellRoute
, при создании StatefulShellRoute
нужно предоставить либо builder
, либо pageBuilder
.
Эти конструкторы отличаются тем, что они принимают параметр StatefulNavigationShell
вместо дочернего виджета. StatefulNavigationShell
можно использовать, чтобы получить доступ к информации о состоянии маршрута или переключить активную ветку. То есть, для восстановления стека навигации другой ветки. Это делается с помощью метода StatefulNavigationShell.goBranch
.
routing/app_routing.dart
final router = GoRouter(
initialLocation: '/notes',
routes: [
// BottomNavigationBar
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
RootScreen(navigationShell: navigationShell),
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/notes',
builder: (context, state) => const NotesScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/favorites',
builder: (context, state) => const FavoritesScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
),
],
);
Объявляем глобальный router
. Для удобства будем применять StatefulShellRoute
с конструктором indexedStack
. В параметре builder
указываем RootScreen
и передаем в него navigationShell
. А в параметре branches
— StatefulShellBranch
с нужными маршрутами.
Теперь создадим навигационную панель на экране RootScreen
. Для этого используем полученный navigationShell
. Из него получаем текущий индекс и маршрут, который указан в ветви маршрута.
root_screen.dart
class RootScreen extends StatelessWidget {
const RootScreen({super.key, required this.navigationShell});
/// Контейнер для навигационного бара.
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
/// Лист элементов для нижнего навигационного бара.
items: _buildBottomNavBarItems,
/// Текущий индекс нижнего навигационного бара.
currentIndex: navigationShell.currentIndex,
/// Обработчик нажатия на элемент нижнего навигационного бара.
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
),
);
}
// Возвращает лист элементов для нижнего навигационного бара.
List<BottomNavigationBarItem> get _buildBottomNavBarItems => [
const BottomNavigationBarItem(
icon: Icon(Icons.note),
label: 'Заметки',
),
const BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Любимые',
),
const BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Профиль',
),
];
}
Дальше нужно добавить в main.dart инициализацию роутера:
main.dart
void main() {
runApp(const AppTodo());
}
class AppTodo extends StatelessWidget {
const AppTodo({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
Инициализируем роутер для приложения. У него есть доступ к контексту и механизм поиска нужного маршрута, поэтому дальше обращаться к методам роутера будем через context
.
Создаем MaterialApp
. Используем в нем go_router вместо Navigator
.
Обратите внимание, что здесь мы не передаем корневой виджет. Теперь всей навигацией управляет go_router. При запуске приложения вы увидите корневой маршрут с вложенной навигацией.
Отлично! Нам осталось создать навигацию детальной информации о заметке.
Реализация маршрута детальной информации
Для этого в экране NotesScreen
создадим моковый список заметок и нарисуем их. По клику мы переходим на экран с детальной информацией о заметке.
Обратите внимание! Переходя на этот экран, мы открываем маршрут, который находится внутри одной ветки маршрутов.
notes_screen.dart
/// Моковый список заметок
const _notes = [
'Note 1',
'Note 2',
'Note 3',
'Note 4',
'Note 5',
'Note 6',
];
class NotesScreen extends StatelessWidget {
const NotesScreen({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ListView.separated(
itemBuilder: (context, index) {
return ListTile(
title: Text(_notes[index]),
onTap: () {
// При клике на заметку переходим на экран
// с детальной информацией
context.go('/notes/detail');
},
);
},
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: _notes.length),
);
}
}
Осталось добавить этот маршрут в карту маршрутов ветки маршрута /notes
.
StatefulShellBranch(
routes: [
GoRoute(
path: '/notes',
builder: (context, state) => const NotesScreen(),
routes: [
GoRoute(
path: 'detail',
builder: (context, state) => const DetailScreen(),
),
]),
],
),
Теперь файл app_routing.dart выглядит так:
app_routing.dart
final router = GoRouter(
initialLocation: '/notes',
routes: [
// BottomNavigationBar
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
RootScreen(navigationShell: navigationShell),
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/notes',
builder: (context, state) => const NotesScreen(),
routes: [
GoRoute(
path: 'detail',
builder: (context, state) => const DetailScreen(),
),
]),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/favorites',
builder: (context, state) => const FavoritesScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
],
),
],
);
Запускаем приложение и получаем такой результат:
В этой статье мы начали изучать библиотеки go_router. Создали нижнюю навигационную панель и научились работать с вложенной навигацией. В следующей части разберем, как использовать редирект, проверять авторизацию, анимировать переходы.
Пример из статьи можно посмотреть по ссылке.
Документация доступна на сайте.
P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!