bottom_shell_nav 0.3.0 copy "bottom_shell_nav: ^0.3.0" to clipboard
bottom_shell_nav: ^0.3.0 copied to clipboard

A route-aware, persistent and adaptive bottom navigation shell for Flutter with go_router/auto_route modes, badges and custom renderers.

example/lib/main.dart

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

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

/// All available appearance presets.
enum BarStyle {
  material('Material'),
  floatingPill('Floating Pill'),
  cupertino('Cupertino'),
  curved('Curved / Notched'),
  gNav('GNav Pill'),
  dotIndicator('Dot Indicator'),
  waterDrop('Water Drop'),
  flashy('Flashy'),
  bubble('Bubble'),
  convex('Convex FAB'),
  sliding('Sliding'),
  glow('Glow / Neon');

  const BarStyle(this.label);
  final String label;

  BottomShellAppearance toAppearance() {
    return switch (this) {
      BarStyle.material => const BottomShellAppearance.material(),
      BarStyle.floatingPill => BottomShellAppearance.floatingPill(),
      BarStyle.cupertino => const BottomShellAppearance.cupertino(),
      BarStyle.curved => BottomShellAppearance.curved(),
      BarStyle.gNav => BottomShellAppearance.gNav(),
      BarStyle.dotIndicator => BottomShellAppearance.dotIndicator(),
      BarStyle.waterDrop => BottomShellAppearance.waterDrop(),
      BarStyle.flashy => BottomShellAppearance.flashy(),
      BarStyle.bubble => BottomShellAppearance.bubble(),
      BarStyle.convex => BottomShellAppearance.convex(),
      BarStyle.sliding => BottomShellAppearance.sliding(),
      BarStyle.glow => BottomShellAppearance.glow(
        backgroundColor: const Color(0xFF1A1B26),
        surfaceColor: const Color(0xFFE9E6F2),
        surfacePadding: const EdgeInsets.all(6),
      ),
    };
  }
}

class CoreModeExampleApp extends StatefulWidget {
  const CoreModeExampleApp({super.key});

  @override
  State<CoreModeExampleApp> createState() => _CoreModeExampleAppState();
}

class _CoreModeExampleAppState extends State<CoreModeExampleApp> {
  BarStyle _currentStyle = BarStyle.floatingPill;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'bottom_shell_nav demo',
      restorationScopeId: 'bottom_shell_nav_core_demo',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
      ),
      home: BottomShell(
        key: ValueKey(_currentStyle),
        restorationScopeId: 'core_shell',
        appearance: _currentStyle.toAppearance(),
        adaptivePolicy: const AdaptiveNavigationPolicy.automatic(
          extendedRailBreakpoint: 840,
          drawerBreakpoint: 1200,
        ),
        scrollToHidePolicy: const ScrollToHidePolicy.enabled(),
        scrollToTopPolicy: const ScrollToTopPolicy.enabled(),
        bodyTransition: const BottomShellBodyTransition.fade(),
        keyboardNavigationPolicy: const KeyboardNavigationPolicy.enabled(),
        hapticFeedbackPolicy: const HapticFeedbackPolicy.selectionClick(),
        selectionGuardPolicy: const SelectionGuardPolicy.showPending(),
        branches: [
          BottomBranch(
            id: 'home',
            destination: const BottomDestination(
              icon: Icons.home_outlined,
              selectedIcon: Icons.home,
              label: 'Home',
            ),
            builder: (_) => DemoBranchPage(
              title: 'Home',
              subtitle: 'Counter state stays alive when you switch tabs.',
              icon: Icons.home,
              currentStyle: _currentStyle,
              onStyleChanged: (style) =>
                  setState(() => _currentStyle = style),
            ),
          ),
          BottomBranch(
            id: 'tasks',
            destination: const BottomDestination(
              icon: Icons.checklist_outlined,
              selectedIcon: Icons.checklist,
              label: 'Tasks',
            ),
            builder: (_) => DemoBranchPage(
              title: 'Tasks',
              subtitle: 'Open a detail page and the bottom bar stays visible.',
              icon: Icons.checklist,
              itemCount: 8,
              currentStyle: _currentStyle,
              onStyleChanged: (style) =>
                  setState(() => _currentStyle = style),
            ),
          ),
          BottomBranch(
            id: 'alerts',
            destination: const BottomDestination(
              icon: Icons.notifications_outlined,
              selectedIcon: Icons.notifications,
              label: 'Alerts',
              badge: BranchBadge.count(8),
            ),
            builder: (_) => DemoBranchPage(
              title: 'Alerts',
              subtitle: 'Badges support count, dot and 99+ rendering.',
              icon: Icons.notifications,
              itemCount: 5,
              currentStyle: _currentStyle,
              onStyleChanged: (style) =>
                  setState(() => _currentStyle = style),
            ),
          ),
          BottomBranch(
            id: 'profile',
            destination: const BottomDestination(
              icon: Icons.person_outline,
              selectedIcon: Icons.person,
              label: 'Profile',
            ),
            builder: (_) => DemoBranchPage(
              title: 'Profile',
              subtitle: 'Tap this selected tab again to pop back to root.',
              icon: Icons.person,
              itemCount: 3,
              currentStyle: _currentStyle,
              onStyleChanged: (style) =>
                  setState(() => _currentStyle = style),
            ),
          ),
        ],
      ),
    );
  }
}

class DemoBranchPage extends StatefulWidget {
  const DemoBranchPage({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.currentStyle,
    required this.onStyleChanged,
    this.itemCount = 4,
    super.key,
  });

  final String title;
  final String subtitle;
  final IconData icon;
  final int itemCount;
  final BarStyle currentStyle;
  final ValueChanged<BarStyle> onStyleChanged;

  @override
  State<DemoBranchPage> createState() => _DemoBranchPageState();
}

class _DemoBranchPageState extends State<DemoBranchPage> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(20, 12, 20, 120),
        children: [
          // Style picker
          Card(
            margin: EdgeInsets.zero,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: Row(
                children: [
                  const Icon(Icons.palette_outlined, size: 20),
                  const SizedBox(width: 12),
                  Expanded(
                    child: DropdownButtonFormField<BarStyle>(
                      initialValue: widget.currentStyle,
                      decoration: const InputDecoration(
                        labelText: 'Bar Style',
                        border: InputBorder.none,
                        isDense: true,
                      ),
                      items: [
                        for (final style in BarStyle.values)
                          DropdownMenuItem(
                            value: style,
                            child: Text(style.label),
                          ),
                      ],
                      onChanged: (style) {
                        if (style != null) widget.onStyleChanged(style);
                      },
                    ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          ListTile(
            contentPadding: EdgeInsets.zero,
            leading: CircleAvatar(
              backgroundColor: colors.primaryContainer,
              foregroundColor: colors.onPrimaryContainer,
              child: Icon(widget.icon),
            ),
            title: Text(widget.title),
            subtitle: Text(widget.subtitle),
          ),
          const SizedBox(height: 16),
          DecoratedBox(
            decoration: BoxDecoration(
              color: colors.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(12),
            ),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      'Branch counter: $_counter',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                  ),
                  FilledButton.icon(
                    onPressed: () => setState(() => _counter++),
                    icon: const Icon(Icons.add),
                    label: const Text('Increment'),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 20),
          Text('Nested routes', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          for (var index = 1; index <= widget.itemCount; index++)
            Card(
              elevation: 0,
              margin: const EdgeInsets.only(bottom: 8),
              child: ListTile(
                leading: Text(index.toString().padLeft(2, '0')),
                title: Text('${widget.title} item $index'),
                subtitle: const Text('Pushes inside this branch navigator.'),
                trailing: const Icon(Icons.chevron_right),
                onTap: () {
                  Navigator.of(context).push(
                    MaterialPageRoute<void>(
                      builder: (_) =>
                          DemoDetailPage(title: '${widget.title} item $index'),
                    ),
                  );
                },
              ),
            ),
        ],
      ),
    );
  }
}

class DemoDetailPage extends StatelessWidget {
  const DemoDetailPage({required this.title, super.key});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(20, 20, 20, 120),
        children: [
          Text(
            'This route was pushed inside the active branch navigator.',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 12),
          const Text(
            'The bottom bar remains visible, switching tabs preserves this '
            'branch stack, and tapping the active tab again pops back to root.',
          ),
          const SizedBox(height: 24),
          FilledButton.icon(
            onPressed: () => Navigator.of(context).pop(),
            icon: const Icon(Icons.arrow_back),
            label: const Text('Back to branch root'),
          ),
        ],
      ),
    );
  }
}
7
likes
0
points
67
downloads

Publisher

unverified uploader

Weekly Downloads

A route-aware, persistent and adaptive bottom navigation shell for Flutter with go_router/auto_route modes, badges and custom renderers.

Repository (GitHub)
View/report issues

Topics

#bottom-navigation #navigation-shell #nested-navigation #go-router #material3

License

unknown (license)

Dependencies

auto_route, flutter, go_router

More

Packages that depend on bottom_shell_nav