Привет, Хабр! Меня зовут Юрий Петров, я 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:

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

Комментарии (9)


  1. BlackJet
    07.11.2023 17:42
    +1

    Auto route классная библиотека, но я не смог победить сброс текущего роута на дефолтный при горячем рестарте. Возможно, я что то делал не так


    1. mrDevGo Автор
      07.11.2023 17:42
      +1

      Да, раньше, не было альтернатив. Из-за этого много проектов делали на auto_route. И что бы перейти на другой тип навигации в проекте, для компании это будет стоить дорого. Вот и тянут дальше эту лямку). Сейчас же есть и навигатор 2 или go_router. Лучше в новых проектах рассмотреть их использование. Ну, или если проект не совсем сложный, попробовать переехать на Navigator.


    1. Dalarin
      07.11.2023 17:42

      Определенно, делал что-то не так. Возможно, создавал инстанс в билд методе)


      1. mrDevGo Автор
        07.11.2023 17:42

        Да, как ни странно, распространенная ошибка )


  1. nikitasalnikov
    07.11.2023 17:42
    -1

    Вообще полная и бессмысленная дичь. Для чего использовать доп библиотеки нагружать приложение ещё какими то библиотеками, когда это все есть под капотом у flutter. Я не понимаю компании которые гонятся за чем то, чего сами походу не понимают. Типа мы в тренде? Мы такие крутые и используем "крутые" библиотеки?. Да и навигатор новичку проще освоить чем сторонние библиотеки.


    1. Sailoc
      07.11.2023 17:42
      +1

      ) для чего вы это написали? Автор же по Русски написал, что не призывает использовать а просто рассказывает как работают автороут. И я вас могу уверить, что в о многих проектах используется автороут. Ну вот встретитесь вы проекте, откроете туториал и будет чуть понятнее). И рано или поздно вы встретите на проекте автороут и еще 5 версии ????


      1. nikitasalnikov
        07.11.2023 17:42
        -1

        Полная брехня. Сколько проектов ни делал. Везде используется нативный роут. Все эти библиотеки лишние захломляют приложение. И даже многие старшие разработчики утверждают что смысла нет добавлять лишние библиотеки, когда можно что то использовать под капотом.


        1. mrDevGo Автор
          07.11.2023 17:42

          Это значит вам сильно повезло????. Но выражать свои мысли можно более конструктивно.


        1. mrDevGo Автор
          07.11.2023 17:42

          Вы можете быть чуток культурнее? И что то мне с трудом верится, что у вас, все проекты на стандартном навигаторе). Сколько у вас было проектов? Если вы думаете в мире Flutter все так розово, то я вас уверяю, что это не так. Есть проекты, где использую еще Redux, есть проекты, прости Господи используют GetX и еще куча все разного. В силу своей работы, приходилось с разными проектами работать. И подход у все разный. И как вы можете писать про автороут если его никогда не встречали и не работали с ним? Вы очень опытный специалист?. По коду определили? Я, не берусь рассуждать про те или иные либы, так как много разного встречал. Ну и если дальше рассуждать, вы стейтменеджеры тоже не используете? ChangeNotife на все приложение? Или кодогенерацию? Зачем, если можно ручками карту di создать. Зачем freezed использовать, если можно самому метод copyWith написать. И так можно продолжать до бесконечности. Сейчас например, сильно развивается go_router командой Flutter. А вы у них не спросили, зачем они это делают если есть стандартный навигатор?