Руководства

Модульная навигация по вкладкам

NavigationBar с независимыми стеками Catalog и Profile, сохранением состояния и явным переключением ветвей.

v1.1.0Средний уровень

Модульная навигация по вкладкам

Готовый Shell содержит Catalog и Profile. Откройте Product в Catalog, перейдите в Profile и вернитесь. Product останется открытым, потому что каждой вкладке принадлежит отдельный стек Navigator.

Итоговое дерево Scope

text
root
└── shell
    ├── catalog-tab  [Catalog, Product]
    └── profile-tab  [Profile]
01

Задайте Scope ID

dart
const rootScopeId = NavigationScopeId('root');
const shellScopeId = NavigationScopeId('shell');
const catalogScopeId = NavigationScopeId('catalog-tab');
const profileScopeId = NavigationScopeId('profile-tab');

Эти ID определяют конкретные стеки. Они не зависят от порядка вкладок в интерфейсе.

02

Экспортируйте Module функциональностей

dart
final catalogModule = NavigationModule(
  id: const NavigationModuleId('catalog'),
  routes: <NavigationRouteDefinitionBase>[catalogRoute, productRoute],
);
final profileModule = NavigationModule(
  id: const NavigationModuleId('profile'),
  routes: <NavigationRouteDefinitionBase>[profileRoute],
);

Module не содержит индекс вкладки, родительский Shell или итоговый префикс URL.

03

Соберите дерево Scope

dart
scopes: const <NavigationScopeDefinition>[
  NavigationScopeDefinition(id: rootScopeId),
  NavigationScopeDefinition(id: shellScopeId, parentId: rootScopeId),
  NavigationScopeDefinition(id: catalogScopeId, parentId: shellScopeId),
  NavigationScopeDefinition(id: profileScopeId, parentId: shellScopeId),
],
mounts: <NavigationMount>[
  shellMount,
  NavigationMount(
    id: const NavigationMountId('catalog-tab'),
    moduleId: catalogModule.id,
    scopeId: catalogScopeId,
    pathPrefix: '/catalog',
  ),
  NavigationMount(
    id: const NavigationMountId('profile-tab'),
    moduleId: profileModule.id,
    scopeId: profileScopeId,
    pathPrefix: '/profile',
  ),
],

Приложение само решает, в каком Scope и под каким URL смонтировать каждый Module. Тот же Catalog Module можно использовать в другом приложении с другим префиксом.

04

Инициализируйте стеки один раз

В didChangeDependencies() один раз вызовите setStack для каждой вкладки, затем активируйте Catalog и Shell. Не выполняйте это в каждом build().

dart
await navigation.setStack(
  const <NavigationTarget<Object?>>[
    NavigationTarget<Object?>(
      destination: CatalogDestination(),
      mountId: NavigationMountId('catalog-tab'),
      scopeId: catalogScopeId,
    ),
  ],
  scopeId: catalogScopeId,
);
await navigation.setStack(
  const <NavigationTarget<Object?>>[
    NavigationTarget<Object?>(
      destination: ProfileDestination(),
      mountId: NavigationMountId('profile-tab'),
      scopeId: profileScopeId,
    ),
  ],
  scopeId: profileScopeId,
);
await navigation.activateBranch(catalogScopeId);
await navigation.activateBranch(shellScopeId);
05

Свяжите NavigationBar с ветвью

dart
return Scaffold(
  body: const NavigationActiveOutlet(parentScopeId: shellScopeId),
  bottomNavigationBar: NavigationBar(
    selectedIndex: active == profileScopeId ? 1 : 0,
    onDestinationSelected: (index) {
      context.readNavigation.activateBranch(
        index == 0 ? catalogScopeId : profileScopeId,
      );
    },
    destinations: const <NavigationDestination>[
      NavigationDestination(icon: Icon(Icons.store), label: 'Catalog'),
      NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
    ],
  ),
);

NavigationActiveOutlet показывает Navigator выбранной дочерней ветви.

06

Проверьте сохранение стеков

Откройте Product с scopeId: catalogScopeId, переключитесь на Profile и вернитесь. При обычном переключении не вызывайте resetBranch: эта операция намеренно уничтожает состояние ветви.

dart
await context.readNavigation.resetBranch(profileScopeId);

Такой сброс уместен, например, после выхода из аккаунта, когда приватная история Profile больше не должна сохраняться.