Pull to refresh
55.83
Friflex
Мобильные приложения для миллионов пользователей🚀

Полное руководство по управлению навигацией во Flutter с помощью пакета go_router: часть 1

Level of difficultyMedium
Reading time5 min
Views4.1K

Привет, Хабр! Меня зовут Юрий Петров, я автор ютуб-канала «Мобильный разработчик» и Flutter Team Lead в Friflex. Мы разрабатываем мобильные приложения для бизнеса и специализируемся на Flutter. 

В этом руководстве я хочу рассказать про пакет go_router. Он помогает управлять навигацией во Flutter. Команда разработки Flutter поддерживает данный пакет. Это позволяет надеяться, что в дальнейшем пакет продолжит развиваться.

Рассказывать буду на примере простого проекта — Todo (заметки). Я понимаю, что таких проектов на Хабре очень много. Но, по-моему, лучшего примера не найти.

Создаем проект и подключаем библиотеку

Немного подумав, я решил, что для нашего проекта необходимо как минимум шесть экранов.

  1. Экран аутентификации пользователя AuthScreen. Будем использовать фейковую аутентификацию.

  2. Экран со списком заметок — NotesScreen.

  3. Экран с детальной информацией заметок — DetailNoteScreen.

  4. Экран с любимыми заметками — FavoriteNotesScreen.

  5. Экран для создания заметок — CreateNoteScreen.

  6. Экран для управления профилем — ProfileScreen.

  7. Экран для размещения рутового экрана с нижней навигационной панелью — 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. А в параметре branchesStatefulShellBranch с нужными маршрутами.

Теперь создадим навигационную панель на экране 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. Создали нижнюю навигационную панель и научились работать с вложенной навигацией. В следующей части разберем, как использовать редирект, проверять авторизацию, анимировать переходы.

Пример из статьи можно посмотреть по ссылке.

Документация доступна на сайте.

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments4

Articles

Information

Website
friflex.com
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
Friflex_dev