adaptive_navigation_view 2.0.1 copy "adaptive_navigation_view: ^2.0.1" to clipboard
adaptive_navigation_view: ^2.0.1 copied to clipboard

A Flutter package for creating adaptive navigation views that seamlessly adapt to different platforms and devices.

Adaptive Navigation View #

A Flutter package that provides a fully adaptive navigation view, inspired by the Fluent Design Navigation View and built on Material 3 principles. The layout automatically adapts between Minimal, Medium, and Expanded display modes based on screen width.

InstallationDisplay ModesUsageNavigation ControllerHierarchical DestinationsThemingKeyboard ShortcutsRTL SupportMigrating from v1Preview


Features #

  • Three Adaptive Display Modes — Minimal (mobile), Medium (tablet), and Expanded (desktop), with customizable breakpoints
  • Smooth Transitions — Animated pane transitions between display modes, no abrupt layout jumps
  • Index or Path Navigation — Select destinations by position index or named path (compatible with GoRouter, Navigator 2.0, FlutterModular)
  • Navigation History — Full navigation history stack with goBack() support and duplicate-aware tracking
  • Hierarchical Destinations — Expandable parent items with collapsible children. In Medium mode, children appear in a floating popup menu.
  • Fully Themeable — Fine-grained control over every visual aspect via NavigationThemeData
  • RTL Support — Full right-to-left language support
  • Keyboard ShortcutsCtrl+B / Cmd+B to toggle the pane, Escape to dismiss
  • Drag Gesture — Swipe to open/close the pane on mobile with fling support
  • Resize Handle — Drag the pane edge on desktop to resize

Installation #

Add the package to your pubspec.yaml:

dependencies:
  adaptive_navigation_view: ^version_number

Or install directly from GitHub:

dependencies:
  adaptive_navigation_view:
    git: https://github.com/GenildoNogueira/adaptive_navigation_view.git

Then run:

flutter pub get

Import in your Dart code:

import 'package:adaptive_navigation_view/adaptive_navigation_view.dart';

Display Modes #

The navigation pane switches between three modes based on available screen width:

Mode Default Width Behavior
Minimal < 600px Only a menu button is shown. The pane slides in as an overlay.
Medium 600px – 840px Icons only when closed. Opens to show icons + labels.
Expanded > 840px Pane is always visible and fully expanded.

You can customize the breakpoints per-instance:

NavigationView(
  compactBreakpoint: const WidthBreakpoint(end: 600),
  mediumBreakpoint: const WidthBreakpoint(start: 600, end: 840),
  expandedBreakpoint: const WidthBreakpoint(start: 840),
  // ...
)

Or force a specific mode regardless of screen width:

NavigationView(
  preferredDisplayMode: DisplayMode.expanded,
  // ...
)

Usage #

Basic Setup #

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
  late final NavigationViewController _controller;
  int _selectedIndex = 0;

  @override
  void initState() {
    super.initState();
    _controller = NavigationViewController(
      length: 3,
      initialIndex: 0,
      destinationType: DestinationTypes.byIndex,
      onDestinationIndex: (index) {
        setState(() => _selectedIndex = index ?? 0);
      },
      vsync: this,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: NavigationView(
        controller: _controller,
        appBar: NavigationAppBar(
          title: const Text('My App'),
        ),
        pane: NavigationPane(
          destinations: const [
            PaneItemDestination(
              icon: Icon(Icons.home_outlined),
              selectedIcon: Icon(Icons.home),
              label: Text('Home'),
            ),
            PaneItemDestination(
              icon: Icon(Icons.explore_outlined),
              selectedIcon: Icon(Icons.explore),
              label: Text('Explore'),
            ),
            PaneItemDestination(
              icon: Icon(Icons.settings_outlined),
              selectedIcon: Icon(Icons.settings),
              label: Text('Settings'),
            ),
          ],
        ),
        body: [
          const Center(child: Text('Home')),
          const Center(child: Text('Explore')),
          const Center(child: Text('Settings')),
        ][_selectedIndex],
      ),
    );
  }
}

NavigationViewController is the central controller for managing navigation state, pane open/close animations, and navigation history.

Index-Based Navigation #

Best for simple, ordered navigation where destinations are identified by position.

final controller = NavigationViewController(
  length: 3,
  initialIndex: 0,
  destinationType: DestinationTypes.byIndex,
  onDestinationIndex: (index) {
    // Called whenever a destination is selected
  },
  vsync: this,
);

// Navigate programmatically
controller.selectDestinationByIndex(2);

// Read current state
print(controller.selectedIndex);   // 2
print(controller.previousIndices); // [0, 2]

Path-Based Navigation #

Best when integrating with named route systems like GoRouter, Navigator 2.0, or FlutterModular.

final controller = NavigationViewController(
  initialPath: '/home',
  destinationType: DestinationTypes.byPath,
  onDestinationPath: (path) {
    // Trigger your router navigation here
    context.go(path!);
  },
  vsync: this,
);

// Navigate programmatically
controller.selectDestinationByPath('/settings');

// Read current state
print(controller.selectedPath);  // '/settings'
print(controller.previousPaths); // ['/home', '/settings']

The controller maintains a full history stack. Navigation by index and by path both support duplicate entries, correctly reflecting how the user actually traversed the app.

// Navigate A → B → A → B
controller.selectDestinationByIndex(1); // history: [0, 1]
controller.selectDestinationByIndex(0); // history: [0, 1, 0]
controller.selectDestinationByIndex(1); // history: [0, 1, 0, 1]

// Go back step by step
controller.goBack(); // history: [0, 1, 0], current: 0
controller.goBack(); // history: [0, 1],    current: 1

// Check if back is available
if (controller.canGoBack) {
  controller.goBack();
}

// Clear history without changing selection
controller.clearHistory();

Pane Control #

controller.open();       // animate open
controller.close();      // animate close
controller.toggle();     // toggle between open/closed
controller.snapOpen();   // instantly open, no animation
controller.snapClosed(); // instantly close, no animation

// Fling (e.g., after a drag gesture)
controller.fling(velocity: 1.0);  // positive = open
controller.fling(velocity: -1.0); // negative = close

// Read state
print(controller.isPaneOpen);  // bool
print(controller.isAnimating); // bool
print(controller.offset);      // 0.0 to 1.0

Hierarchical Destinations #

Parent destinations with children are not directly selectable — tapping them expands or collapses their sub-items. Only leaf items (those without children) are navigable.

In Medium mode with the pane closed, parent items show their children in a floating popup menu instead.

NavigationViewController(
  length: 4, // count only leaf (selectable) destinations
  initialPath: '/',
  destinationType: DestinationTypes.byPath,
  vsync: this,
)

NavigationPane(
  destinations: [
    PaneItemDestination(
      icon: const Icon(Icons.folder_outlined),
      selectedIcon: const Icon(Icons.folder),
      label: const Text('Documents'),
      initialExpanded: true, // expanded on first render
      children: [
        PaneItemDestination(
          icon: const Icon(Icons.insert_drive_file_outlined),
          label: const Text('Files'),
          path: '/documents/files',
        ),
        PaneItemDestination(
          icon: const Icon(Icons.image_outlined),
          label: const Text('Images'),
          path: '/documents/images',
        ),
      ],
    ),
    PaneItemDestination(
      icon: const Icon(Icons.settings_outlined),
      selectedIcon: const Icon(Icons.settings),
      label: const Text('Settings'),
      path: '/settings',
    ),
  ],
)

Items placed in footers are pinned to the bottom of the pane and do not scroll with the main destinations:

NavigationPane(
  destinations: [ /* main items */ ],
  footers: [
    PaneItemDestination(
      icon: const Icon(Icons.help_outline),
      label: const Text('Help'),
      path: '/help',
    ),
  ],
)

Accessing the Controller from a Child Widget #

// From anywhere in the subtree
final state = NavigationView.of(context);
state.openPane();
state.closePane();

// Or nullable version
NavigationView.maybeOf(context)?.openPane();

Theming #

Apply a theme to the entire navigation via NavigationTheme:

NavigationTheme(
  data: NavigationThemeData(
    openWidth: 280,
    compactWidth: 72,
    indicatorColor: Colors.blue.shade100,
    indicatorShape: const StadiumBorder(),
    indicatorSize: const Size.fromHeight(40),
    itemAnimationDuration: const Duration(milliseconds: 250),
    itemAnimationCurve: Curves.easeInOutCubic,
    itemMargin: const EdgeInsets.symmetric(horizontal: 12),
    itemContentPadding: const EdgeInsets.symmetric(horizontal: 8),
    itemShape: WidgetStateProperty.all(const StadiumBorder()),
    itemSelectedBackgroundColor: Colors.blue.shade50,
    itemHoverBackgroundColor: Colors.black.withValues(alpha: 0.04),
    itemChevronColor: Colors.grey,
    itemChildrenIndent: 20,
  ),
  child: NavigationView(/* ... */),
)

All NavigationThemeData properties support lerp for smooth theme transitions. You can also use copyWith to override only specific values:

final myTheme = NavigationThemeData(
  openWidth: 300,
).copyWith(
  indicatorColor: Colors.purple,
  itemAnimationDuration: const Duration(milliseconds: 300),
);

Available Theme Properties #

Category Properties
Pane backgroundColor, elevation, shadowColor, surfaceTintColor, shape, minimalShape, scrimColor
Sizes openWidth, compactWidth, itemSize, indicatorSize, itemChevronSize, itemChildrenIndent, itemChildrenSpacing
Indicator indicatorColor, indicatorShape
Item Background itemBackgroundColor, itemSelectedBackgroundColor, itemHoverBackgroundColor, itemPressedBackgroundColor
Item Shape itemShape, itemMargin, itemContentPadding, itemSpacing, itemElevation, itemShadowColor
Icons iconTheme, itemIconColor, itemSelectedIconColor, itemHoverIconColor, itemDisabledIconColor, itemIconSize
Labels labelTextStyle, itemLabelStyle, itemSelectedLabelStyle, itemHoverLabelStyle, itemDisabledLabelStyle
Chevron itemChevronColor, itemChevronHoverColor, itemSelectedChevronColor
Animation itemAnimationDuration, itemAnimationCurve

Keyboard Shortcuts #

Shortcut Action
Ctrl+B / Cmd+B (macOS) Toggle pane open/closed
Escape Close/dismiss the pane
/ Navigate between items when focused

RTL Support #

The navigation pane automatically mirrors its layout for right-to-left languages. No extra configuration is needed beyond setting up your app's locale:

MaterialApp(
  supportedLocales: const [
    Locale('en', 'US'), // LTR
    Locale('ar', 'AR'), // RTL — pane appears on the right
    Locale('he', 'IL'), // RTL
  ],
  localizationsDelegates: GlobalMaterialLocalizations.delegates,
  home: NavigationView(/* ... */),
)

Migrating from v1 #

Controller is now required #

In v1, navigation state was handled internally by NavigationPane via selectedIndex and onDestinationSelected. In v2, you must create and manage a NavigationViewController yourself and pass it to NavigationView.

// v1
NavigationView(
  pane: NavigationPane(
    selectedIndex: _selectedIndex,
    onDestinationSelected: (i) => setState(() => _selectedIndex = i),
    children: [ /* ... */ ],
  ),
)

// v2
NavigationView(
  controller: _controller, // required
  pane: NavigationPane(
    destinations: [ /* ... */ ], // renamed from children
  ),
)

Rename PaneThemeNavigationTheme #

// v1
PaneTheme(
  data: PaneThemeData(openWidth: 280),
  child: NavigationView(/* ... */),
)

// v2
NavigationTheme(
  data: NavigationThemeData(openWidth: 280),
  child: NavigationView(/* ... */),
)
// v1
NavigationPane(children: [ PaneItemDestination(/* ... */) ])

// v2
NavigationPane(destinations: [ PaneItemDestination(/* ... */) ])

Path-based navigation #

If you use GoRouter or any named-route system, you can now replace index tracking with path-based navigation entirely — no need to manually sync _selectedIndex with your router state:

final controller = NavigationViewController(
  initialPath: '/home',
  destinationType: DestinationTypes.byPath,
  onDestinationPath: (path) => context.go(path!),
  vsync: this,
);

Preview #

Minimal (Mobile) #

Medium (Tablet) #

Expanded (Desktop) #


Acknowledgements #

Built on the principles and guidelines of Material 3 and inspired by the Fluent Design Navigation View.

4
likes
160
points
279
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package for creating adaptive navigation views that seamlessly adapt to different platforms and devices.

Repository (GitHub)
View/report issues

Topics

#navigation #adaptive #material #fluent-design #responsive

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on adaptive_navigation_view