
Привет, Хабр! Меня зовут Юрий Петров, я Flutter Team Lead в Friflex и автор ютуб-канала «Юрий Петров | Всё об IT». Мы разрабатываем мобильные приложения для бизнеса и специализируемся на Flutter. В этой статье хочу рассказать про библиотеку auto_route, с помощью которой можно управлять навигацией во Flutter. Статья не призывает к использованию данной библиотеки, но будет полезна, для тех кто встретится с данной библиотекой в своих проектах.
История навигации во Flutter
Flutter, как быстро развивающийся фреймворк, имеет много подходов для управления навигацией. На данный момент самые популярные:
Есть и другие подходы, их можно найти в Pub.dev. Я ни в коем случае не хочу сравнивать эти библиотеки, так как все они являются трудом разработчиков, и имеют свое место под солнцем.
В самом SDK Flutter есть прекрасный и удобный навигатор (Navigator), с помощью которого можно реализовывать всё, что написано в данной статье. Но для новичка иногда трудно понять все нюансы встроенного навигатора. Для быстрой реализации разработчики и придумывают разные фреймворки для навигации. По этой же причине более двух лет назад и был создан auto_route, который успешно развивается и активно используется в проектах. Для примера напишем простое приложение, где попробуем реализовать все возможности данной библиотеки.
auto_route: начало
Для начала добавляем библиотеки в файл pubspec.yaml в раздел dependencies проекта:
auto_route — сама библиотека
И в раздел dev_dependencies все, что связанно с генерацией.
auto_route_generator — генератор роутов
build_runner — библиотека для кодогенерации
dependencies: flutter: sdk: flutter auto_route: ^7.8.4 cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter auto_route_generator: ^7.3.2 build_runner: ^2.4.6
Теперь давайте попробуем инициализировать наш роутер. Добавим три экрана и один корневой. А также, аннотацию @RoutePage, данная аннотация указывает, что для экрана необходимо создать роут, который в дальнейшем можно будет добавить в схему роутов. Для удобства создадим папку features, где разделим наше приложение на три небольшие features:

root_screen.dart
@RoutePage() class RootScreen extends StatelessWidget { const RootScreen({super.key}); @override Widget build(BuildContext context) { return const Placeholder(); } }
profile_screen.dart
@RoutePage() class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Профиль')), ); } }
my_books_screen.dart
@RoutePage() class MyBooksScreen extends StatelessWidget { const MyBooksScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Мои книги')), ); } }
Далее создаем файл app_router.dart

app_router.dart
part 'app_router.gr.dart'; @AutoRouterConfig(replaceInRouteName: 'Screen,Route') class AppRouter extends _$AppRouter { @override List<AutoRoute> get routes => []; }
Обратите внимание на параметр replaceInRouteName: 'Screen,Route'. Это указывает на то, что при создании роута будет меняться слово Screen на Route. Например, из экрана ListBooksScreen создается роут ListBooksRoute. Также, можно указать более сложное условие, например: {Name1}|{Name2}|{Name3}, {ReplacementName}. Например: "Modal|Screen|Dialog|Page, Route". Это значит, что слова Modal, Screen, Dialog и Page будут заменены на Route.
Ничего страшного, если на этом этапе будут ошибки. После выполнения кодогенерации ошибки исчезнут.
Далее переходим в консоль и запускаем команду:
dart run build_runner build --delete-conflicting-outputs
После ее выполнения у вас появится новый сгенерированный файл app_router.gr.dart:

В этом файле хранятся сгенерированные роуты для наших экранов. В нем лучше ничего не трогать. Если не подтянутся импорты, то пройдите в этот файл и импортируйте недостающие данные.
Теперь осталось добавить роуты в геттер routes в файле app_router.dart.
Но, в большинстве случаев нам необходимо добавить нижний навигационный бар, для этого нам необходимо реализовать вложенную (nested) навигацию. Давайте сразу так и сделаем. Поправим немного файл app_router.dart.
app_router.dart
@AutoRouterConfig(replaceInRouteName: 'Screen,Route') class AppRouter extends _$AppRouter { @override List<AutoRoute> get routes => [ /// Основной, корневой маршрут AutoRoute( page: RootRoute.page, initial: true, children: [ /// Вложенные маршруты AutoRoute(page: ListBooksRoute.page, initial: true), AutoRoute(page: MyBooksRoute.page), AutoRoute(page: Profile Route.page), ], ), ]; }
Добавляем в корневой экран нижний навигационный бар и специальный виджет AutoTabsScaffold для упрощения создания интерфейсов с вкладками (tabs). Данный виджет позволяет удобно связывать вкладки с различными экранами или маршрутами в приложении. Также, автоматически управляет навигацией между маршрутами, связанными с вкладками. Это особенно полезно в приложениях с множеством различных экранов.
root_screen.dart
@RoutePage() class RootScreen extends StatelessWidget { const RootScreen({super.key}); @override Widget build(BuildContext context) { return AutoTabsScaffold( routes: const [ ListBooksRoute(), MyBooksRoute(), ProfileRoute(), ], bottomNavigationBuilder: (_, tabsRouter) { return BottomNavigationBar( currentIndex: tabsRouter.activeIndex, onTap: tabsRouter.setActiveIndex, items: const [ BottomNavigationBarItem( label: 'Все книги', icon: Icon(Icons.book), ), BottomNavigationBarItem( label: 'Мои книги', icon: Icon(Icons.book_online), ), BottomNavigationBarItem( label: 'Профиль', icon: Icon(Icons.verified_user), ), ], ); }, ); } }
Осталось поправить точку входа в приложение.
main.dart
final appRouter = AppRouter(); void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: appRouter.config(), ); } }
В строках:
1 - Инициализируем роутер, который будет использоваться в приложении. Так как у него есть доступ к контексту и механизм поиска нужного роута, в дальнейшем будем использовать context для обращения к методам AppRouter.
12- Создаем MaterialApp, в котором используем AppRouter вместо Navigator
Обратите внимание, что здесь мы не передаем корневой виджет. Теперь навигацией управляет appRouter. При запуске приложения вы увидите корневой роут с вложенной навигацией.
Результат

Вот таким несложным способом мы реализовали нижний навигационный бар и инициализировали роутер.
Вложенная навигация внутри навигации
Попробуем улучшить наше приложение и добавим список книг. При тапе на любой элемент из списка мы должны перейти на экран данной книги, а внутри этого экрана можно будет перейти на экран настройки. Но хотелось бы отметить, что это приложение исключительно для изучения навигации. Так что логика будет вся “моковая”.
Добавляем в папку list_books два экрана about_book_screen.dart и settings_book_screen.dart. И аналогично добавляем их в роутинг. Далее реализуем вызов AboutBookScreen из ListBooksScreen, а вызов SettingsBookScreen — из AboutBookScreen.
about_book_screen.dart
@RoutePage() class AboutBookScreen extends StatelessWidget { const AboutBookScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'О книге', ), actions: [ IconButton( onPressed: () { context.router.push(const SettingsBookRoute()); }, icon: const Icon(Icons.settings), ) ], ), ); } }
settings_book_screen.dart
@RoutePage() class SettingsBookScreen extends StatelessWidget { const SettingsBookScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Настройки книги')), ); } }
Теперь встает вопрос, как добавить эти экраны, чтобы они открывались только во вкладке «Все книги» — то есть, чтобы новый экран не закрывал нижний навигационный бар.
Для этого вынесем все роуты features lits_books в отдельный файл и создадим обертку. В дальнейшем я более подробно опишу обертки в auto_route. Пока просто добавим обертку ListBooksWrapperScreen, которая реализует интерфейс AutoRouteWrapper.
list_books_wrapper_screen.dart
@RoutePage() class ListBooksWrapperScreen extends StatelessWidget implements AutoRouteWrapper { const ListBooksWrapperScreen({super.key}); @override Widget build(BuildContext context) { return const AutoRouter(); } @override Widget wrappedRoute(BuildContext context) { return this; } }
Вызываем кодогенерацию, командой:
dart run build_runner build --delete-conflicting-outputs
Далее для удобства создаем список роутов в абстрактном классе ListBooksRoutes в файле list_books_routes.dart:
list_books_routes.dart
abstract class ListBooksRoutes { static final routes = AutoRoute( page: ListBooksWrapperRoute.page, children: [ AutoRoute(page: ListBooksRoute.page, initial: true), AutoRoute(page: AboutBookRoute.page), AutoRoute(page: SettingsBookRoute.page), ], ); }
Теперь мы видим, что мы создали список роутов. Корневым роутом стал ListBooksWrapperRoute, а инициализирующим — ListBooksRoute. Меняем список роутов в app_router.dart. Теперь класс AppRouter выглядит вот так:
app_router.dart
part 'app_router.gr.dart'; @AutoRouterConfig(replaceInRouteName: 'Screen,Route') class AppRouter extends _$AppRouter { @override List<AutoRoute> get routes => [ /// Основной, корневой маршрут AutoRoute( page: RootRoute.page, initial: true, children: [ /// Вложенные маршруты ListBooksRoutes.routes, AutoRoute(page: MyBooksRoute.page), AutoRoute(page: ProfileRoute.page), ], ), ]; }
Осталось поправить root_screen.dart, так как теперь мы будем вызывать ListBooksWrapperRoute вместо ListBooksRoute:
root_screen.dart
@RoutePage() class RootScreen extends StatelessWidget { const RootScreen({super.key}); @override Widget build(BuildContext context) { return AutoTabsScaffold( routes: const [ ListBooksWrapperRoute(), MyBooksRoute(), ProfileRoute(), ], bottomNavigationBuilder: (_, tabsRouter) { return BottomNavigationBar( currentIndex: tabsRouter.activeIndex, onTap: tabsRouter.setActiveIndex, items: const [ BottomNavigationBarItem( label: 'Все книги', icon: Icon(Icons.book), ), BottomNavigationBarItem( label: 'Мои книги', icon: Icon(Icons.book_online), ), BottomNavigationBarItem( label: 'Профиль', icon: Icon(Icons.verified_user), ), ], ); }, ); } }
Смотрим результат:

Таким образом мы с вами начали изучение библиотеки auto_route: создали нижний навигационный бар и научились работать с вложенной навигацией. В следующей части разберем, как использовать Guards и AutoRouteWrapper.
Пример из данной статьи можно посмотреть на GitHub.
Документация доступна на сайте https://autoroute.vercel.app/introduction
P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!
