Другое

Flutter GoRouter: Полное руководство по навигации по веткам

Освойте навигацию по веткам в Flutter GoRouter с StatefulShellRoute. Научитесь переключаться между экранами Город и Место, сохраняя корректное поведение кнопки возврата и состояние.

Как реализовать навигацию по веткам в GoRouter Flutter с корректным поведением кнопки “Назад”?

Я работаю с GoRouter в Flutter и мне нужно осуществлять навигацию между ветками, сохраняя правильную функциональность кнопки “Назад”. Конкретно:

  1. Навигация от ветки 1 (экран City) к ветке 2 (экран Venue)
  2. Обеспечение смены ветки во время навигации
  3. Гарантия того, что кнопка “Назад” возвращает к ветке 1 (экран City)

Из моих исследований кажется, что этот шаблон навигации по веткам не поддерживается напрямую GoRouter. Требуется ли для этого ручная реализация или существует встроенное решение для управления переходами между ветками и поведением стека возврата?

Реализация навигации по ветвям в GoRouter Flutter с корректной работой кнопки “Назад”

Для реализации навигации по ветвям в GoRouter Flutter с корректной работой кнопки “Назад” необходимо использовать StatefulShellRoute и StatefulShellBranch вместе с методом goBranch(). Это встроенное решение позволяет переключаться между ветвями, сохраняя состояние и корректное поведение истории навигации.


Содержание


Понимание Stateful Shell Routes

StatefulShellRoute - это решение GoRouter для управления состоятельной вложенной навигации с несколькими ветвями. В отличие от обычного ShellRoute, StatefulShellRoute поддерживает состояние навигации для каждой ветви отдельно, что именно то, что вам нужно для ветвей экранов City и Venue.

Согласно CodeWithAndrea, “StatefulShellRoute принимает список элементов StatefulShellBranch, каждый из которых представляет отдельную состоятельную ветвь”. Это означает, что каждая ветвь может иметь собственную историю навигации и независимо поддерживать состояние.

Основные компоненты:

  • StatefulShellRoute: Основной контейнер для навигации по ветвям
  • StatefulShellBranch: Представляет отдельные ветви навигации (например, экран City, экран Venue)
  • StatefulNavigationShell: Предоставляет доступ к методам навигации по ветвям

Реализация навигации по ветвям

Настройка конфигурации роутера

Инициализируйте GoRouter с использованием StatefulShellRoute:

dart
final GoRouter _router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/city', // Начать с ветви City
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithBottomNav(navigationShell: navigationShell);
      },
      branches: [
        // Ветвь экрана City
        StatefulShellBranch(
          navigatorKey: _cityNavigatorKey,
          routes: [
            GoRoute(
              path: '/city',
              builder: (context, state) => const CityScreen(),
            ),
            // Вложенные маршруты экрана City
          ],
        ),
        // Ветвь экрана Venue  
        StatefulShellBranch(
          navigatorKey: _venueNavigatorKey,
          routes: [
            GoRoute(
              path: '/venue',
              builder: (context, state) => const VenueScreen(),
            ),
            // Вложенные маршруты экрана Venue
          ],
        ),
      ],
    ),
  ],
);

Навигация между ветвями

Используйте метод goBranch() для переключения между ветвями с сохранением текущего состояния внутри каждой ветви:

dart
// С экрана City на экран Venue
StatefulNavigationShell.of(context).goBranch(1); // 1 = индекс ветви Venue

// С экрана Venue обратно на экран City  
StatefulNavigationShell.of(context).goBranch(0); // 0 = индекс ветви City

Как объясняется на Blup.in, “вы можете вызывать StatefulNavigationShell.of(context).goBranch(index) из любого подмаршрута StatefulShellRoute. Понимая, как программно переключаться между ветвями, вы получаете больше контроля над навигацией пользователя.”


Обработка поведения кнопки “Назад”

Поведение кнопки “Назад” по умолчанию

Поведение кнопки “Назад” по умолчанию работает корректно с StatefulShellRoute, поскольку каждая ветвь поддерживает собственный стек навигации. Когда пользователи нажимают кнопку “Назад”, они перемещаются назад по истории текущей ветви.

Реализация пользовательской кнопки “Назад”

Если вам нужна пользовательская реализация кнопки “Назад”, вы можете использовать следующий шаблон:

dart
class CityScreen extends StatelessWidget {
  const CityScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Экран City'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            // Навигация назад к предыдущему экрану в текущей ветви
            context.pop();
          },
        ),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Переход к ветви Venue
            StatefulNavigationShell.of(context).goBranch(1);
          },
          child: const Text('Перейти к Venue'),
        ),
      ),
    );
  }
}

Кнопка “Назад” в браузере

Для обработки кнопки “Назад” в браузере вам может потребоваться реализовать пользовательские наблюдатели. Как показано в одном из ответов на Stack Overflow, вы можете использовать:

dart
GoRouter(
  observers: [
    NavigationRouteObserver(),
  ],
  routes: // ...
)

Полный пример реализации

Вот полный рабочий пример, реализующий ветви экранов City и Venue:

dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

// Ключи навигатора для каждой ветви
final GlobalKey<NavigatorState> _cityNavigatorKey = GlobalKey();
final GlobalKey<NavigatorState> _venueNavigatorKey = GlobalKey();

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

final GoRouter _router = GoRouter(
  navigatorKey: GlobalKey(), // Корневой навигатор
  initialLocation: '/city',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithBottomNav(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          navigatorKey: _cityNavigatorKey,
          routes: [
            GoRoute(
              path: '/city',
              builder: (context, state) => const CityScreen(),
              routes: [
                GoRoute(
                  path: 'details/:id',
                  builder: (context, state) => CityDetailScreen(
                    cityId: int.parse(state.pathParameters['id']!),
                  ),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          navigatorKey: _venueNavigatorKey,
          routes: [
            GoRoute(
              path: '/venue',
              builder: (context, state) => const VenueScreen(),
              routes: [
                GoRoute(
                  path: 'details/:id',
                  builder: (context, state) => VenueDetailScreen(
                    venueId: int.parse(state.pathParameters['id']!),
                  ),
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
}

class ScaffoldWithBottomNav extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const ScaffoldWithBottomNav({
    super.key,
    required this.navigationShell,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        onTap: (index) {
          // Навигация к выбранной ветви
          navigationShell.goBranch(index);
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.location_city),
            label: 'City',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.venue),
            label: 'Venue',
          ),
        ],
      ),
    );
  }
}

class CityScreen extends StatelessWidget {
  const CityScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Города'),
      ),
      body: ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Город ${index + 1}'),
            onTap: () {
              // Навигация к деталям города внутри ветви City
              context.go('/city/details/${index + 1}');
            },
          );
        },
      ),
    );
  }
}

class VenueScreen extends StatelessWidget {
  const VenueScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Площадки'),
      ),
      body: ListView.builder(
        itemCount: 8,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Площадка ${index + 1}'),
            onTap: () {
              // Навигация к деталям площадки внутри ветви Venue
              context.go('/venue/details/${index + 1}');
            },
          );
        },
      ),
    );
  }
}

class CityDetailScreen extends StatelessWidget {
  final int cityId;

  const CityDetailScreen({super.key, required this.cityId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Детали города - $cityId'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID города: $cityId'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Переход к ветви Venue с деталей City
                StatefulNavigationShell.of(context).goBranch(1);
              },
              child: const Text('Перейти к ветви Venue'),
            ),
          ],
        ),
      ),
    );
  }
}

class VenueDetailScreen extends StatelessWidget {
  final int venueId;

  const VenueDetailScreen({super.key, required this.venueId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Детали площадки - $venueId'),
      ),
      body: Center(
        child: Text('ID площадки: $venueId'),
      ),
    );
  }
}

Распространенные проблемы и решения

1. Ветвь не изменяется при навигации

Проблема: Ветвь фактически не изменяется при вызове goBranch().

Решение: Убедитесь, что вы используете StatefulShellRoute.indexedStack и что индекс ветви правильный. Метод goBranch() должен вызываться для StatefulNavigationShell, полученного из контекста.

2. Кнопка “Назад” работает некорректно

Проблема: Кнопка “Назад” не возвращает к ожидаемому экрану.

Решение:

  • Убедитесь, что у каждой ветви есть свой navigatorKey
  • Избегайте смешивания context.go() с goBranch() для межветвевой навигации
  • Используйте context.pop() только для внутриветвевой навигации

3. Состояние не сохраняется между переключениями ветвей

Проблема: Состояние экрана теряется при переключении ветвей.

Решение: StatefulShellRoute разработан для сохранения состояния. Если состояние все еще теряется, проверьте, что вы не перестраиваете навигационный оболочку без необходимости.

4. Проблемы с кнопкой “Назад” системы Android

Проблема: Кнопка “Назад” системы Android закрывает приложение вместо навигации назад.

Решение: Согласно руководству Miyuru Sanjana, вам может потребоваться реализовать правильную обработку PopScope:

dart
PopScope(
  canPop: true,
  onPopInvoked: (didPop) {
    if (!didPop) {
      // Логика обработки нажатия кнопки "Назад"
    }
  },
  child: YourScreen(),
)

Продвинутые шаблоны навигации по ветвям

Динамическая навигация по ветвям

Иногда необходимо перейти к конкретным экранам внутри ветвей:

dart
// Навигация к конкретному экрану в ветви Venue
StatefulNavigationShell.of(context).goBranch(
  1, // индекс ветви Venue
  initialLocation: '/venue/details/5', // конкретный экран
);

Вложенные Stateful Shell Routes

Для сложных иерархий навигации можно вкладывать StatefulShellRoutes:

dart
StatefulShellRoute(
  builder: (context, state, nestedNavigationShell) {
    return NestedShell(navigationShell: nestedNavigationShell);
  },
  branches: [
    // Определения ветвей...
  ],
)

Обработка глубоких ссылок (Deep Linking)

GoRouter автоматически обрабатывает глубокие ссылки с StatefulShellRoute:

dart
// URL: /city/details/3
// Это откроет ветвь City и перейдет к экрану деталей с id=3

Как объясняется на Dhiwise, “с этим вы можете иметь несколько навигаторов ветвей, каждый со своей активной ветвью, что позволяет создавать сложные шаблоны навигации, такие как глубокая ссылка и вложенная навигация.”


Источники

  1. Flutter Bottom Navigation Bar with Stateful Nested Routes using GoRouter
  2. Stateful Nested Navigation in Flutter: Using GoRouter’s StatefulShellRoute and StatefulShellBranch
  3. Creating Flutter bottom navigation bar with routes
  4. How to Fix Flutter GoRouter PopScope Issue on Android
  5. Enhancing Navigation in Flutter with StatefulShellRoute
  6. Flutter GoRouter browser back button navigation issue
  7. Flutter Web - Fix Refresh & Back button Problem

Заключение

Реализация навигации по ветвям в GoRouter Flutter с корректной работой кнопки “Назад” хорошо поддерживается через API StatefulShellRoute и StatefulShellBranch. Вот ключевые выводы:

  1. Используйте StatefulShellRoute: Это встроенное решение для управления состоятельной навигацией по ветвям. Оно поддерживает отдельные стеки навигации для каждой ветви.

  2. Реализуйте goBranch(): Используйте StatefulNavigationShell.of(context).goBranch(index) для программного переключения между ветвями.

  3. Корректные ключи навигатора: Назначайте уникальный navigatorKey каждому StatefulShellBranch для обеспечения правильной обработки кнопки “Назад”.

  4. Сохранение состояния: StatefulShellRoute автоматически сохраняет состояние внутри каждой ветви при переключении между ними.

  5. Обработка платформо-специфичных проблем: Реализуйте правильную обработку для кнопки “Назад” системы Android и кнопки “Назад” браузера при необходимости.

Шаблон, который вы ищете (экран City на экран Venue с корректной навигацией “Назад”) не требует ручной реализации - StatefulShellRoute GoRouter предоставляет именно эту функциональность “из коробки”. Следуя шаблонам реализации, показанным выше, вы можете создать бесшовный опыт навигации между ветвями City и Venue, поддерживая корректное поведение кнопки “Назад” на всех платформах.

Авторы
Проверено модерацией
Модерация