πŸ“± Multilevel Navigator

Адаптивная навигационная систСма для Flutter с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ 4 ΡƒΡ€ΠΎΠ²Π½Π΅ΠΉ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ

Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° для создания Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½Ρ‹Ρ… ΠΌΠ½ΠΎΠ³ΠΎΡƒΡ€ΠΎΠ²Π½Π΅Π²Ρ‹Ρ… интСрфСйсов (list β†’ detail β†’ detail β†’ detail) с автоматичСской подстройкой количСства Π²ΠΈΠ΄ΠΈΠΌΡ‹Ρ… ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ ΠΏΠΎΠ΄ ΡˆΠΈΡ€ΠΈΠ½Ρƒ экрана. ΠŸΠΎΡΡ‚Ρ€ΠΎΠ΅Π½Π° ΠΏΠΎΠ²Π΅Ρ€Ρ… go_router.


✨ ВозмоТности

  • 4-уровнСвая навигация (level1 β€” панСль Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ, level2 β€” список, level3/level4 β€” Π΄Π΅Ρ‚Π°Π»ΠΈ) с автоматичСским расчётом видимости ΡƒΡ€ΠΎΠ²Π½Π΅ΠΉ ΠΏΠΎΠ΄ ΡˆΠΈΡ€ΠΈΠ½Ρƒ экрана
  • Resizable-ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ β€” ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ тянСт Ρ€Π°Π·Π΄Π΅Π»ΠΈΡ‚Π΅Π»ΡŒ ΠΈ мСняСт ΡˆΠΈΡ€ΠΈΠ½Ρƒ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ (SplitPane); ΠΌΠΎΠΆΠ½ΠΎ Π·Π°Π΄Π°Ρ‚ΡŒ ΠΈ Π½Π°Ρ‡Π°Π»ΡŒΠ½ΡƒΡŽ ΡˆΠΈΡ€ΠΈΠ½Ρƒ ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ per-Ρ„ΠΈΡ‡Π°
  • Π‘Π²ΠΎΡ€Π°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅ level2/level3/level4 β€” ΠΏΠΎ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΌΡƒ Ρ‚Π°ΠΏΡƒ Π½Π° Π°ΠΊΡ‚ΠΈΠ²Π½ΡƒΡŽ Ρ„ΠΈΡ‡Ρƒ (Level2CollapseMode) ΠΈΠ»ΠΈ Π½Π° ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ… ΠΏΡ€ΠΈ ΡƒΡ…ΠΎΠ΄Π΅ с уровня (MobileLevelCollapseMode), с сохранСниСм состояния Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Π°/BLoC
  • Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ состояния:
    • Π·Π°ΠΏΠΎΠΌΠΈΠ½Π°Π΅Ρ‚ ΠΏΠΎΠ»Π½Ρ‹ΠΉ URI ΠΊΠ°ΠΆΠ΄ΠΎΠΉ Ρ„ΠΈΡ‡ΠΈ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ„ΠΈΡ‡Π°ΠΌΠΈ
    • сохраняСт Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹ ΠΈ BLoC ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΏΠΎΠΊΠ°Π·ΠΎΠΌ/скрытиСм уровня (PersistentBlocProvider)
  • ΠœΠΎΠ΄Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΎΠΊΠ½Π° Ρ‡Π΅Ρ€Π΅Π· query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ ?modal=id, с Ρ€Π°Π·Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ Π±ΠΈΠ»Π΄Π΅Ρ€Π°ΠΌΠΈ ΠΏΠΎΠ΄ ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΡƒ (mobile/desktop/web) ΠΈ ΡˆΠΈΡ€ΠΈΠ½Ρƒ экрана (dialog/bottom sheet)
  • БистСма Ρ‚Π°Π±ΠΎΠ² с автоматичСской синхронизациСй Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ Ρ‚Π°Π±Π° с URL
  • Авторизация β€” Ρ€Π°Π·Π΄Π΅Π»Π΅Π½ΠΈΠ΅ Ρ„ΠΈΡ‡ Π½Π° Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΠ΅ ΠΈ Π½Π΅ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‰ΠΈΠ΅ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ, Ρ€Π΅Π΄ΠΈΡ€Π΅ΠΊΡ‚Ρ‹ Ρ‡Π΅Ρ€Π΅Π· AuthenticationStateNotifier
  • Dependency Injection β€” DI-ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ ΠΏΠΎΠ²Π΅Ρ€Ρ… InheritedWidget, скрытый Π·Π° MultilevelNavigatorProvider

πŸ—οΈ АрхитСктура

Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ:

lib/
β”œβ”€β”€ core/                           # Π―Π΄Ρ€ΠΎ
β”‚   β”œβ”€β”€ cache/                      # LRU-кэш (Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠΉ ΠΌΠ΅Ρ…Π°Π½ΠΈΠ·ΠΌ, Π½Π΅ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ API β€” см. Π½ΠΈΠΆΠ΅)
β”‚   β”œβ”€β”€ contracts/                  # AuthenticationStateNotifier, RouterParams
β”‚   β”œβ”€β”€ models/                     # Level, DisplayMode, VisibilityMap, FeatureMultilevelRules, Level2CollapseMode, MobileLevelCollapseMode
β”‚   β”œβ”€β”€ providers/                  # PersistentBlocProvider
β”‚   β”œβ”€β”€ utils/                      # MultilevelDimensions, NavigationUtils, NavigationStorage, PathUtils
β”‚   └── validators/                 # Валидация ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Ρ„ΠΈΡ‡
β”‚
β”œβ”€β”€ features/                       # Π€ΠΈΡ‡ΠΈ ΠΏΠ°ΠΊΠ΅Ρ‚Π° (Clean Architecture)
β”‚   β”œβ”€β”€ multilevel_layout/            # Расчёт layout: MultilevelBloc, LevelsWidthBloc, SplitPane
β”‚   β”œβ”€β”€ modal_system/                # ΠœΠΎΠ΄Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΎΠΊΠ½Π° Ρ‡Π΅Ρ€Π΅Π· query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ `modal`
β”‚   └── navigation/                 # Π’Π°Π±Ρ‹, back button handler, mixins
β”‚
β”œβ”€β”€ dependency_injection/           # MultilevelNavigatorDependencies (InheritedWidget)
β”œβ”€β”€ configuration/                  # FeatureNavigationConfiguration, FeatureLevelItemConfiguration
β”œβ”€β”€ router/                         # MultilevelNavigator, построСниС GoRouter
β”œβ”€β”€ extensions/                     # context.goNamed/pop/openModal/switchToFeature/...
└── multilevel_navigator.dart         # Public API (barrel-Ρ„Π°ΠΉΠ»)

πŸš€ Быстрый старт

1. Установка

dependencies:
  multilevel_navigator: ^1.0.0

2. Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ прилоТСния

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // ΠŸΠΎΡ€ΡΠ΄ΠΎΠΊ Π²Π°ΠΆΠ΅Π½: сначала Π½Π΅Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ„ΠΈΡ‡ΠΈ, ΠΏΠΎΡ‚ΠΎΠΌ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Π΅
  final features = [
    AuthenticationNavigationConfiguration(),
    ChatsNavigationConfiguration(),
    CallsNavigationConfiguration(),
    ProfileNavigationConfiguration(),
  ];

  final multilevelNavigator = MultilevelNavigator(
    features: features,
    authenticationStateNotifier: AuthenticationStateNotifierImpl(authRepository),
    // navigationPanelBuilder обязатСлСн β€” Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° Π½Π΅ рисуСт level1 "ΠΈΠ· ΠΊΠΎΡ€ΠΎΠ±ΠΊΠΈ",
    // ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ само строит панСль Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ (sidebar/bottom nav) для ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠ³ΠΎ
    // StatefulNavigationShell.
    navigationPanelBuilder: (navigationShell) => NavigationPanel(navigationShell: navigationShell),
    header: null,   // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ header Π½Π°Π΄ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ΠΎΠΌ
    overlay: null,  // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ overlay ΠΏΠΎΠ²Π΅Ρ€Ρ… всСго layout
  );

  runApp(
    MultilevelNavigatorProvider(
      multilevelNavigator: multilevelNavigator,
      child: MaterialApp.router(
        title: 'My App',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true),
        routerConfig: multilevelNavigator.router,
      ),
    ),
  );
}

Π Π°Π±ΠΎΡ‡ΠΈΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ со всСми Ρ„ΠΈΡ‡Π°ΠΌΠΈ (chats, calls, profile, authentication, guest) Π»Π΅ΠΆΠΈΡ‚ Π² example/.


πŸ“‹ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Ρ„ΠΈΡ‡ΠΈ

КаТдая Ρ„ΠΈΡ‡Π° прилоТСния Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ FeatureNavigationConfiguration:

final class ChatsNavigationConfiguration implements FeatureNavigationConfiguration {
  const ChatsNavigationConfiguration();

  @override
  String? get initialLocation => null; // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Π½Π°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΡƒΡ‚ΡŒ Ρ„ΠΈΡ‡ΠΈ

  @override
  bool get isVisibleInNavigation => true;

  @override
  bool get requiresAuthentication => true;

  @override
  String name(BuildContext context) => 'Π§Π°Ρ‚Ρ‹'; // ΠΌΠ΅Ρ‚ΠΎΠ΄, Π° Π½Π΅ Π³Π΅Ρ‚Ρ‚Π΅Ρ€ β€” ΠΌΠΎΠΆΠ½ΠΎ Π»ΠΎΠΊΠ°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· context

  @override
  String get key => 'chats'; // ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ Ρ„ΠΈΡ‡ΠΈ (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π² switchToFeature ΠΈ Ρ‚.ΠΏ.)

  @override
  IconData get emoji => Icons.chat;

  @override
  String get basePath => 'chats';

  @override
  FeatureLevelItemConfiguration get level2 => FeatureLevelItemConfiguration(
    path: '',
    name: 'chats_level2',
    builder: (context) => const ChatsLevel2Page(),
    placeholderBuilder: (context) => const ChatLevel2Placeholder(),
  );

  @override
  List<FeatureLevelItemConfiguration> get level3 => [
    FeatureLevelItemConfiguration(
      path: ':chatId',
      name: 'chats_level3_messages',
      builder: (context) => ChatsLevel3MessagesPage(chatId: _chatId(context)),
      placeholderBuilder: (context) => const ChatLevel3Placeholder(),
      parentName: 'chats_level2',
    ),
  ];

  @override
  List<FeatureLevelItemConfiguration> get level4 => const [];

  @override
  List<MultilevelModalBuilderAbstraction> get modals => [ModalCall(), ModalVideo()];

  // ΠŸΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹, доступныС ΠΈΠ· BuildContext Π²ΠΎ всСх уровнях (level2-4) Ρ„ΠΈΡ‡ΠΈ.
  // ΠŸΡ€ΠΈ implements (Π² ΠΎΡ‚Π»ΠΈΡ‡ΠΈΠ΅ ΠΎΡ‚ extends) этот Π³Π΅Ρ‚Ρ‚Π΅Ρ€ обязатСлСн.
  @override
  List<SingleChildWidget> get providers => const [];
}

ΠžΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ Ρ‡Π»Π΅Π½Ρ‹ ΠΊΠΎΠ½Ρ‚Ρ€Π°ΠΊΡ‚Π° ΠΈΠΌΠ΅ΡŽΡ‚ значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ (пСрСопрСдСляйтС ΠΏΠΎ нСобходимости):

Π§Π»Π΅Π½ НазначСниС
defaultLevel2Path ΠΏΡƒΡ‚ΡŒ level2 Ρ„ΠΈΡ‡ΠΈ, ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ /$basePath
rules FeatureMultilevelRules β€” ΠΊΠ°ΠΊΠΈΠ΅ ΡƒΡ€ΠΎΠ²Π½ΠΈ Π²ΠΈΠ΄Π½Ρ‹ ΠΏΡ€ΠΈ ΠΊΠ°ΠΊΠΎΠΉ Π³Π»ΡƒΠ±ΠΈΠ½Π΅ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ Π² ΠΊΠ°ΠΆΠ΄ΠΎΠΌ ΠΈΠ· Ρ€Π΅ΠΆΠΈΠΌΠΎΠ² ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ FeatureMultilevelDefaultRules)
collapseMode Level2CollapseMode.remove (ΡƒΠ±Ρ€Π°Ρ‚ΡŒ level2 ΠΈΠ· Π΄Π΅Ρ€Π΅Π²Π°) ΠΈΠ»ΠΈ .hide (ΡΠΊΡ€Ρ‹Ρ‚ΡŒ, ΡˆΠΈΡ€ΠΈΠ½Π° 0, сохранив state level3/level4) ΠΏΡ€ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΌ Ρ‚Π°ΠΏΠ΅ Π½Π° Π°ΠΊΡ‚ΠΈΠ²Π½ΡƒΡŽ Ρ„ΠΈΡ‡Ρƒ
collapseAnimationDuration Π΄Π»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ Π°Π½ΠΈΠΌΠ°Ρ†ΠΈΠΈ сворачивания level2 (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для .hide)
level3CollapseMode / level4CollapseMode MobileLevelCollapseMode.preserve (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ, состояниС сохраняСтся) ΠΈΠ»ΠΈ .discard (обычная навигация Π½Π°Π·Π°Π΄) ΠΏΡ€ΠΈ сворачивании level3/level4 Π½Π° ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ…
initialLevel2Width / initialLevel3Width / initialLevel4Width Π½Π°Ρ‡Π°Π»ΡŒΠ½Π°Ρ ΡˆΠΈΡ€ΠΈΠ½Π° ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ Π² пиксСлях ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ построСнии layout Ρ„ΠΈΡ‡ΠΈ (Π΄ΠΎ Ρ€ΡƒΡ‡Π½ΠΎΠ³ΠΎ resize); для level3 дСйствуСт ΠΊΠ°ΠΊ минимально гарантированная ΡˆΠΈΡ€ΠΈΠ½Π°

FeatureLevelItemConfiguration (level2/level3/level4-элСмСнт): path, name (ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½ΠΎΠ΅ имя Ρ€ΠΎΡƒΡ‚Π°), builder, placeholderBuilder (рСндСрится, ΠΊΠΎΠ³Π΄Π° ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ скрыт ΠΏΠΎ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ), parentName (связываСт ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ с Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΌ), providers (доступны Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π² ΠΏΠΎΠ΄Π΄Π΅Ρ€Π΅Π²Π΅ этого уровня).


🧩 ΠΠ΄Π°ΠΏΡ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ: ΡƒΡ€ΠΎΠ²Π½ΠΈ, Π±Ρ€Π΅ΠΉΠΊΠΏΠΎΠΈΠ½Ρ‚Ρ‹, ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ

Π’ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ ΡƒΡ€ΠΎΠ²Π½Π΅ΠΉ опрСдСляСтся количСством ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ умСщаСтся Π² ΡˆΠΈΡ€ΠΈΠ½Ρƒ экрана (MultilevelDimensions, lib/core/utils/multilevel_dimensions.dart):

level1Width = 50     // панСль Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ
level2Width = 420
level3MinWidth = 750 // level3 β€” гибкая ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ°, тянСт ΠΎΡΡ‚Π°Π²ΡˆΠ΅Π΅ΡΡ мСсто, это ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ
level4Width = 250

widthForTwoColumns   = 800
widthForThreeColumns = level1Width + level2Width + level3MinWidth   // = 1220
widthForFourColumns  = widthForThreeColumns + level4Width           // = 1470

ΠŸΠΎΡ€ΠΎΠ³ΠΈ Ρ‚Ρ€Ρ‘Ρ…- ΠΈ Ρ‡Π΅Ρ‚Ρ‹Ρ€Ρ‘Ρ…ΠΊΠΎΠ»ΠΎΠ½ΠΎΡ‡Π½ΠΎΠ³ΠΎ Ρ€Π΅ΠΆΠΈΠΌΠ° Π²Ρ‹Ρ‡ΠΈΡΠ»ΡΡŽΡ‚ΡΡ ΠΈΠ· ΡˆΠΈΡ€ΠΈΠ½ ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ, Π° Π½Π΅ Π·Π°Π΄Π°Π½Ρ‹ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ константами β€” ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ level2Width сдвигаСт ΠΈ ΠΏΠΎΡ€ΠΎΠ³ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π° Π½Π° 3 ΠΊΠΎΠ»ΠΎΠ½ΠΊΠΈ.

ЀактичСский Ρ€Π΅ΠΆΠΈΠΌ отобраТСния β€” DisplayMode (5 Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ, Π° Π½Π΅ просто Β«N ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊΒ»):

  • floatingPanel β€” ΡƒΠ·ΠΊΠΈΠΉ экран, level1 ΠΏΠΎΠΊΠ°Π·Π°Π½ ΠΊΠ°ΠΊ ΠΏΠ»Π°Π²Π°ΡŽΡ‰Π°Ρ панСль
  • singleColumn β€” ΠΎΠ΄Π½Π° ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° Π½Π° вСсь экран
  • singleColumnWithBottomNav β€” ΠΎΠ΄Π½Π° ΠΊΠΎΠ»ΠΎΠ½ΠΊΠ° + ниТняя навигация
  • fixedMultiColumn β€” нСсколько ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ фиксированной ΡˆΠΈΡ€ΠΈΠ½Ρ‹
  • resizableMultiColumn β€” нСсколько ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ с Ρ€ΡƒΡ‡Π½Ρ‹ΠΌ resize (SplitPane)

КакиС ΠΈΠΌΠ΅Π½Π½ΠΎ ΡƒΡ€ΠΎΠ²Π½ΠΈ (ΠΈ плСйсхолдСры) Π²ΠΈΠ΄Π½Ρ‹ ΠΏΡ€ΠΈ Π΄Π°Π½Π½ΠΎΠΌ Ρ€Π΅ΠΆΠΈΠΌΠ΅ ΠΈ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ Π³Π»ΡƒΠ±ΠΈΠ½Π΅ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ (1–4), Ρ€Π΅ΡˆΠ°Π΅Ρ‚ FeatureMultilevelRules.{single,two,three,four}ColumnVisibility(activeDepth) β€” Ρ„ΠΈΡ‡Π° ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ rules, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π°Π΄Π°Ρ‚ΡŒ ΡΠΎΠ±ΡΡ‚Π²Π΅Π½Π½ΡƒΡŽ ΠΌΠ°Ρ‚Ρ€ΠΈΡ†Ρƒ видимости вмСсто FeatureMultilevelDefaultRules.


πŸ’Ύ Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ состояния

  • URI Ρ„ΠΈΡ‡ΠΈ запоминаСтся ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ„ΠΈΡ‡Π°ΠΌΠΈ ΠΈ восстанавливаСтся ΠΏΡ€ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π΅ (NavigationStorage, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ автоматичСски Π² context.switchToFeature).
  • BLoC уровня, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΠΎΠΆΠ΅Ρ‚ Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ ΡΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒΡΡ ΠΏΠΎ Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½Ρ‹ΠΌ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ, сохраняСтся Ρ‡Π΅Ρ€Π΅Π· PersistentBlocProvider вмСсто ΠΎΠ±Ρ‹Ρ‡Π½ΠΎΠ³ΠΎ BlocProvider:
FeatureLevelItemConfiguration(
  path: ':chatId',
  name: 'chats_level3_messages',
  builder: (context) => const ChatsLevel3MessagesPage(),
  placeholderBuilder: (context) => const ChatLevel3Placeholder(),
  providers: [
    PersistentBlocProvider<ChatMessagesBloc>(
      cacheKey: 'chat_messages_${GoRouterState.of(context).pathParameters['chatId']}',
      create: (context) => ChatMessagesBloc()..add(LoadMessages()),
    ),
  ],
)

Π˜Π½ΡΡ‚Π°Π½Ρ BLoC ΠΆΠΈΠ²Ρ‘Ρ‚ Π² кэшС Π½Π° ΡƒΡ€ΠΎΠ²Π½Π΅ Ρ„ΠΈΡ‡ΠΈ (FeatureBlocCacheScope) ΠΈ ΠΏΠ΅Ρ€Π΅ΠΆΠΈΠ²Π°Π΅Ρ‚ скрытиС/ΠΏΠΎΠΊΠ°Π· уровня β€” состояниС Π½Π΅ сбрасываСтся, ΠΏΠΎΠΊΠ° сама Ρ„ΠΈΡ‡Π° смонтирована.


πŸ” Авторизация

class AuthenticationStateNotifierImpl extends AuthenticationStateNotifier {
  AuthenticationStateNotifierImpl(this._authRepository) {
    _subscription = _authRepository.authStateStream.listen((_) => notifyListeners());
  }

  final AuthRepository _authRepository;
  StreamSubscription? _subscription;

  @override
  Future<bool> get isAuthenticated => _authRepository.isAuthenticated();

  @override
  Future<bool> get isGuest async => false; // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ: гостСвой Ρ€Π΅ΠΆΠΈΠΌ

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
}

AuthenticationStateNotifier β€” это ChangeNotifier с двумя Π³Π΅Ρ‚Ρ‚Π΅Ρ€Π°ΠΌΠΈ (isAuthenticated, isGuest), Π° Π½Π΅ с ΠΌΠ΅Ρ‚ΠΎΠ΄Π°ΠΌΠΈ. Π€ΠΈΡ‡ΠΈ с requiresAuthentication: false (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, экран Π»ΠΎΠ³ΠΈΠ½Π° ΠΈΠ»ΠΈ гостСвой Ρ€Π΅ΠΆΠΈΠΌ) доступны Π±Π΅Π· Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ; ΠΏΡ€ΠΈ true β€” Ρ€ΠΎΡƒΡ‚Π΅Ρ€ Ρ€Π΅Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΈΡ‚ Π½Π΅Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ. МоТно завСсти любоС количСство Π½Π΅Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Ρ… Ρ„ΠΈΡ‡.


πŸͺŸ ΠœΠΎΠ΄Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΎΠΊΠ½Π°

Модалки ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ ?modal=id ΠΈ рСндСрятся ΠΊΠ°ΠΊ dialog ΠΈΠ»ΠΈ bottom sheet β€” Ρ€Π°Π·Π΄Π΅Π»ΡŒΠ½ΠΎ для ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ…/дСсктопа/Π²Π΅Π±Π° ΠΈ для ΠΎΠ΄Π½ΠΎ-/ΠΌΠ½ΠΎΠ³ΠΎΠΊΠΎΠ»ΠΎΠ½ΠΎΡ‡Π½ΠΎΠ³ΠΎ Ρ€Π΅ΠΆΠΈΠΌΠ°. Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΠ΅ΠΌΡ‹ΠΉ API β€” MultilevelModalPlatformBuilder:

class ModalCall implements MultilevelModalPlatformBuilder {
  @override
  String get id => 'call';

  @override
  bool get isDismissible => true;

  @override
  PlatformModalBuilders? get mobile => PlatformModalBuilders(
    singleColumn: PlatformWidthModalBuilders(
      bottomSheetBuilder: (context) => const CallBottomSheet(),
    ),
    multiColumn: PlatformWidthModalBuilders(
      dialogBuilder: (context) => const CallDialog(),
    ),
  );

  @override
  PlatformModalBuilders get desktop => PlatformModalBuilders(
    singleColumn: PlatformWidthModalBuilders(dialogBuilder: (context) => const CallDialog()),
    multiColumn: PlatformWidthModalBuilders(dialogBuilder: (context) => const CallDialog()),
  );

  @override
  PlatformModalBuilders? get web => desktop;
}

РСгистрируСтся Π² modals Ρ„ΠΈΡ‡ΠΈ (см. ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Π²Ρ‹ΡˆΠ΅) ΠΈ открываСтся/закрываСтся Ρ‡Π΅Ρ€Π΅Π· контСкст:

context.openModal('call');
context.closeModal('call');

// с ΠΎΠΆΠΈΠ΄Π°Π½ΠΈΠ΅ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°
final result = await context.openModalAsync<String>('call');
context.closeModalWithResult('call', 'done');

// с ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ»ΡŒΠ½Ρ‹ΠΌΠΈ Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚Π°ΠΌΠΈ вмСсто query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ²
context.openModalWithArgs('call', callId);
final callId = context.readModalArgs<String>('call'); // Π²Π½ΡƒΡ‚Ρ€ΠΈ Π±ΠΈΠ»Π΄Π΅Ρ€Π° ΠΌΠΎΠ΄Π°Π»ΠΊΠΈ

Π‘Ρ‚Π°Ρ€Ρ‹ΠΉ интСрфСйс MultilevelModalBuilder (Π΅Π΄ΠΈΠ½Ρ‹ΠΉ dialogBuilder/bottomSheetBuilder Π±Π΅Π· раздСлСния ΠΏΠΎ ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ΅) ΠΏΠΎΠΌΠ΅Ρ‡Π΅Π½ @Deprecated β€” ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ MultilevelModalPlatformBuilder Π² Π½ΠΎΠ²ΠΎΠΌ ΠΊΠΎΠ΄Π΅.


πŸ—‚οΈ Π’Π°Π±Ρ‹

TabManagerConfiguration Π°Π²Ρ‚ΠΎΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½ΠΎΠ΅ имя query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π° ΠΈΠ· Ρ‚ΠΈΠΏΠ° страницы (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ChatsLevel2Page β†’ chats_level2_page_tab), Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ нСсколько нСзависимых Π½Π°Π±ΠΎΡ€ΠΎΠ² Ρ‚Π°Π±ΠΎΠ² Π½Π° Ρ€Π°Π·Π½Ρ‹Ρ… уровнях Π½Π΅ ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ΡƒΡŽΡ‚:

class ChatsLevel2Page extends StatefulWidget {
  const ChatsLevel2Page({super.key});
  @override
  State<ChatsLevel2Page> createState() => _ChatsLevel2PageState();
}

class _ChatsLevel2PageState extends State<ChatsLevel2Page>
    with TickerProviderStateMixin, TabControllerMixin {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    initializeTabController(
      TabManagerConfiguration(
        ChatsLevel2Page,
        tabs: const [
          TabConfiguration(name: 'all', title: 'ВсС', icon: Icons.chat, content: AllChatsTab()),
          TabConfiguration(name: 'unread', title: 'НСпрочитанныС', icon: Icons.mark_chat_unread, content: UnreadChatsTab()),
        ],
      ),
    );
    syncTabControllerWithQuery();
  }

  @override
  Widget build(BuildContext context) {
    syncTabControllerWithQuery();
    return MultilevelTabContent(
      tabController: tabController,
      configuration: configuration!,
      onTabChanged: switchToTab,
    );
  }
}

🧭 Навигация ΠΈΠ· ΠΊΠΎΠ΄Π°

ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ доступны ΠΊΠ°ΠΊ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ Π½Π° BuildContext (ΡΠΎΡ…Ρ€Π°Π½ΡΡŽΡ‚ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠ΅ query-ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹/extra/Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅):

context.goNamed('chats_level3_messages', pathParameters: {'chatId': id});
context.pop(level: 3);                 // Π·Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ level3 ΠΈ всё, Ρ‡Ρ‚ΠΎ Π²Ρ‹ΡˆΠ΅
context.switchToFeature(ChatsNavigationConfiguration()); // с восстановлСниСм сохранённого URI Ρ„ΠΈΡ‡ΠΈ

context.toggleLevel2();                // ΡΠ²Π΅Ρ€Π½ΡƒΡ‚ΡŒ/Ρ€Π°Π·Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒ level2 (Level2CollapseMode.hide)
context.collapseMobileLevel3();        // ΡΠ²Π΅Ρ€Π½ΡƒΡ‚ΡŒ level3 Π½Π° мобильном, сохранив state (ΠΈΠ»ΠΈ .discard β€” см. level3CollapseMode)
context.expandMobileLevel3();

βš™οΈ Π’Π½ΡƒΡ‚Ρ€Π΅Π½Π½Π΅Π΅ устройство (Π½Π΅ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ API)

  • DI β€” MultilevelNavigatorDependencies (InheritedWidget) собираСт MultilevelBloc, back-button handler ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ зависимости; ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ скрыт Π·Π° MultilevelNavigatorProvider, ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡŽ ΠΎΠ±Ρ€Π°Ρ‰Π°Ρ‚ΡŒΡΡ ΠΊ Π½Π΅ΠΌΡƒ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ.
  • Кэш-ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ (core/cache/*, LRU с TTL/статистикой) ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π²Π½ΡƒΡ‚Ρ€ΠΈ MultilevelBloc ΠΈ SplitPaneCalculator для ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ вычислСний видимости/layout/сохранённых ΡˆΠΈΡ€ΠΈΠ½ ΠΊΠΎΠ»ΠΎΠ½ΠΎΠΊ. Π­Ρ‚ΠΎ внутрСнняя инфраструктура ΠΏΠ°ΠΊΠ΅Ρ‚Π° β€” классы CacheManagerImpl/CacheKeys/CacheNames Π½Π΅ ΡΠΊΡΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΡŽΡ‚ΡΡ ΠΈΠ· package:multilevel_navigator/multilevel_navigator.dart ΠΈ Π½Π΅ ΠΏΡ€Π΅Π΄Π½Π°Π·Π½Π°Ρ‡Π΅Π½Ρ‹ для использования Π² ΠΊΠΎΠ΄Π΅ прилоТСния.

πŸ§ͺ ВСстированиС

ВСстами ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ΠΎ ядро адаптивности:

test/
β”œβ”€β”€ core/cache/cache_manager_test.dart
β”œβ”€β”€ core/utils/navigation_utils_test.dart
β”œβ”€β”€ core/utils/path_utils_test.dart
β”œβ”€β”€ features/multilevel_layout/application/bloc/multilevel_bloc_test.dart
β”œβ”€β”€ features/multilevel_layout/presentation/widgets/split/split_pane_test.dart
└── router/feature_configuration_resolver_test.dart

Модальная систСма, Ρ‚Π°Π±Ρ‹, DI-ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ ΠΈ PersistentBlocProvider тСстами ΠΏΠΎΠΊΠ° Π½Π΅ ΠΏΠΎΠΊΡ€Ρ‹Ρ‚Ρ‹.


πŸ“œ Π˜ΡΡ‚ΠΎΡ€ΠΈΡ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ

ΠΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ список ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ ΠΏΠΎ вСрсиям β€” Π² CHANGELOG.md.

Libraries

configuration/feature_level_configuration
configuration/feature_navigation_configuration
core/cache/cache_container
core/cache/cache_key
core/cache/cache_keys
core/cache/cache_manager
core/cache/cache_manager_contract
core/cache/cache_name
core/cache/cache_names
core/cache/cache_stats
core/constants/display_mode_constants
core/constants/level_constants
core/contracts/authentication_state_notifier
core/contracts/router_params
core/factories/router_factory
core/models/display_mode
core/models/feature_multilevel_default_rules
core/models/feature_multilevel_rules
core/models/level
core/models/level2_collapse_mode
core/models/mobile_level_collapse_mode
core/models/multilevel_layout_rule
core/models/visibility_map
core/providers/persistent_bloc_provider
core/utils/multilevel_dimensions
core/utils/path_utils
core/utils/validation_utils
core/validators/feature_configuration_validator
dependency_injection/multilevel_navigator_dependencies
extensions/context_navigation_extensions
extensions/multilevel_navigator_extensions
features/modal_system/application/managers/multilevel_modal_manager
features/modal_system/domain/models/multilevel_modal_builder
features/modal_system/domain/models/multilevel_modal_builder_abstraction
features/modal_system/domain/models/platform_modal_builders
features/modal_system/domain/models/platform_width_modal_builders
features/modal_system/presentation/mixins/auto_close_popup_mixin
features/modal_system/presentation/widgets/multilevel_bottom_sheet_wrapper
features/modal_system/presentation/widgets/multilevel_dialog_wrapper
features/modal_system/presentation/widgets/multilevel_modal_overlay
features/multilevel_layout/application/bloc/levels_width_bloc
features/multilevel_layout/application/bloc/multilevel_bloc
features/multilevel_layout/domain/models/levels_width
features/multilevel_layout/domain/services/layout_calculator_service
features/multilevel_layout/domain/services/visibility_resolver_service
features/multilevel_layout/infrastructure/observers/route_change_observer
features/multilevel_layout/infrastructure/observers/screen_metrics_observer
features/multilevel_layout/infrastructure/services/layout_calculator_service_impl
features/multilevel_layout/infrastructure/services/visibility_resolver_service_impl
features/multilevel_layout/presentation/layouts/authorized_scope
features/multilevel_layout/presentation/layouts/multi_column_layout
features/multilevel_layout/presentation/layouts/multilevel_layout
features/multilevel_layout/presentation/layouts/simple_row_layout
features/multilevel_layout/presentation/layouts/single_column_layout
features/multilevel_layout/presentation/layouts/split_pane_layout
features/multilevel_layout/presentation/utils/split_pane_calculator
features/multilevel_layout/presentation/widgets/default/default_not_found
features/multilevel_layout/presentation/widgets/default/default_placeholder
features/multilevel_layout/presentation/widgets/split/split_pane
features/multilevel_layout/presentation/widgets/split/utils
features/multilevel_layout/presentation/widgets/split_pane_widget
features/multilevel_layout/presentation/widgets/splitter_widget
features/navigation/application/handlers/back_button_handler
features/navigation/domain/models/tab_configuration
features/navigation/domain/models/tab_manager_configuration
features/navigation/presentation/mixins/tab_controller_mixin
features/navigation/presentation/widgets/feature_bloc_cache
features/navigation/presentation/widgets/feature_scope
features/navigation/presentation/widgets/feature_state_scope
features/navigation/presentation/widgets/multilevel_tab_bar
features/navigation/presentation/widgets/multilevel_tab_content
features/navigation/presentation/widgets/multilevel_tab_page
multilevel_navigator
multilevel_navigator_provider
multilevel_navigator_scope
router/current_feature_configuration
router/feature_configuration_resolver
router/multilevel_navigator
router/page_builder
router/route/auth_route
router/route/unauth_route
router/route_builder