declarative_nav 0.0.1 copy "declarative_nav: ^0.0.1" to clipboard
declarative_nav: ^0.0.1 copied to clipboard

Declarative navigation helpers (Navigator 2.0 pages) and local overlays without Navigator.push/pop or showDialog.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: DemoMenu());
  }
}

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

  @override
  State<DemoMenu> createState() => _DemoMenuState();
}

class _DemoMenuState extends State<DemoMenu> {
  bool _showTabsDemo = false;

  @override
  Widget build(BuildContext context) {
    if (_showTabsDemo) {
      return TabsDemoRoot(onExit: () => setState(() => _showTabsDemo = false));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('declarative_nav examples')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              onPressed: () => Navigator.of(context).push(
                MaterialPageRoute(builder: (_) => const NoTabsDemoRoot()),
              ),
              child: const Text('No-tabs: setState pages + overlay (PageEntry as data)'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () => setState(() => _showTabsDemo = true),
              child: const Text('Tabs: setState per-tab pages + local overlays (tab,pageKey)'),
            ),
          ],
        ),
      ),
    );
  }
}

/// ----------------------------
/// No-tabs demo (setState pages + overlay)
/// ----------------------------
class NoTabsDemoRoot extends StatefulWidget {
  const NoTabsDemoRoot({super.key});

  @override
  State<NoTabsDemoRoot> createState() => _NoTabsDemoRootState();
}

class _NoTabsDemoRootState extends State<NoTabsDemoRoot> {
  OverlayRequest? _overlay;
  late List<PageEntry> _pages;

  @override
  void initState() {
    super.initState();
    _pages = const [
      PageEntry(key: 'home', name: '/home'),
    ];
  }

  void _openDialog() {
    setState(() => _overlay = DialogRequest(key: 'hello', barrierDismissible: true));
  }

  void _dismissOverlay() {
    setState(() => _overlay = null);
  }

  void _goDetail() {
    setState(() {
      _pages = [..._pages, const PageEntry(key: 'detail', name: '/detail')];
    });
  }

  void _popTopPage() {
    if (_pages.length <= 1) return;
    setState(() {
      _pages = _pages.sublist(0, _pages.length - 1);
    });
  }

  Widget _buildPage(BuildContext context, PageEntry page) {
    switch (page.key) {
      case 'home':
        return _HomePage(
          onOpenDialog: _openDialog,
          onGoDetail: _goDetail,
        );
      case 'detail':
        return const _DetailPage(title: 'Detail (no-tabs)');
      default:
        return const SizedBox.shrink();
    }
  }

  @override
  Widget build(BuildContext context) {
    return DeclarativePagesBackScope(
      pages: _pages,
      popTopPage: _popTopPage,
      isOverlayVisible: _overlay != null,
      dismissOverlay: _dismissOverlay,
      child: AnimatedScreenOverlayHost(
        overlay: _overlay,
        onDismiss: _dismissOverlay,
        overlayBuilder: (context, req, dismiss) {
          switch (req) {
            case DialogRequest(key: 'hello'):
              return Padding(
                padding: const EdgeInsets.all(16),
                child: IntrinsicWidth(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text('Hello! (no showDialog)'),
                      const SizedBox(height: 12),
                      ElevatedButton(onPressed: dismiss, child: const Text('Close')),
                    ],
                  ),
                ),
              );
            default:
              return null;
          }
        },
        child: DeclarativePagesNavigator(
          pages: _pages,
          buildPage: _buildPage,
          onPopTop: _popTopPage,
          canPopTop: () => _overlay == null,
        ),
      ),
    );
  }
}

class _HomePage extends StatelessWidget {
  final VoidCallback onOpenDialog;
  final VoidCallback onGoDetail;

  const _HomePage({
    required this.onOpenDialog,
    required this.onGoDetail,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home (no-tabs)')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(
              onPressed: onOpenDialog,
              child: const Text('Open dialog'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: onGoDetail,
              child: const Text('Go detail'),
            ),
          ],
        ),
      ),
    );
  }
}

class _DetailPage extends StatelessWidget {
  final String title;
  const _DetailPage({required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: const Center(child: Text('Use system back or gesture to pop.')),
    );
  }
}

/// ----------------------------
/// Tabs demo (per-tab pages + per-tab-page overlay map)
/// ----------------------------
const tabHome = TabId('home');
const tabSettings = TabId('settings');

class TabsDemoRoot extends StatefulWidget {
  final VoidCallback onExit;
  const TabsDemoRoot({super.key, required this.onExit});

  @override
  State<TabsDemoRoot> createState() => _TabsDemoRootState();
}

class _TabsDemoRootState extends State<TabsDemoRoot> {
  TabId _currentTab = tabHome;

  // Per-tab pages
  late Map<TabId, List<PageEntry>> _pagesByTab;

  // ✅ Screen-local overlays keyed by (tab, pageKey)
  final Map<TabPageKey, OverlayRequest?> _overlaysByScreen = {};

  @override
  void initState() {
    super.initState();
    _pagesByTab = {
      tabHome: [
        PageEntry(key: 'home_root', name: '/home'),
      ],
      tabSettings: [
        PageEntry(key: 'settings_root', name: '/settings'),
      ],
    };
  }

  void _setPages(TabId tab, List<PageEntry> pages) {
    setState(() {
      _pagesByTab = {..._pagesByTab, tab: pages};
    });
  }

  // Current screen key: (currentTab, topPageKey)
  TabPageKey get _currentScreenKey {
    final pages = _pagesByTab[_currentTab]!;
    return (_currentTab, pages.last.key);
  }

  OverlayRequest? get _currentOverlay => _overlaysByScreen[_currentScreenKey];

  void _openDialogForCurrentScreen() {
    final key = _currentScreenKey;
    setState(() {
      _overlaysByScreen[key] = DialogRequest(key: 'hello', barrierDismissible: true);
    });
  }

  void _dismissCurrentOverlay() {
    final key = _currentScreenKey;
    setState(() {
      _overlaysByScreen.remove(key);
    });
  }

  void _goDetailInCurrentTab() {
    final pages = _pagesByTab[_currentTab]!;
    final nextKey = '${pages.last.key}_detail';

    _setPages(
      _currentTab,
      [
        ...pages,
        PageEntry(
          key: nextKey,
          name: '${pages.last.name}/detail',
          data: {'from': pages.last.key},
        ),
      ],
    );
  }

  Widget _buildTabPage(BuildContext context, TabId tab, PageEntry page) {
    // Build widgets based on (tab, page.key) or page.name.
    // This is where you keep UI separate from navigation state.
    if (tab == tabHome && page.key == 'home_root') {
      return _TabRootPage(
        title: 'Home tab root',
        onOpenDialog: _openDialogForCurrentScreen,
        onGoDetail: _goDetailInCurrentTab,
      );
    }
    if (tab == tabSettings && page.key == 'settings_root') {
      return _TabRootPage(
        title: 'Settings tab root',
        onOpenDialog: _openDialogForCurrentScreen,
        onGoDetail: _goDetailInCurrentTab,
      );
    }

    // Any *_detail pages
    if (page.key.endsWith('_detail')) {
      return _DetailPage(title: '${tab.value} detail: ${page.data}');
    }

    return _DetailPage(title: 'Unknown page: ${page.key}');
  }

  @override
  Widget build(BuildContext context) {
    final overlay = _currentOverlay;

    return AnimatedScreenOverlayHost(
      overlay: overlay,
      onDismiss: _dismissCurrentOverlay,
      overlayBuilder: (context, req, dismiss) {
        switch (req) {
          case DialogRequest(key: 'hello'):
            return Padding(
              padding: const EdgeInsets.all(16),
              child: IntrinsicWidth(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Text('Hello (screen-local per tab/page)!'),
                    const SizedBox(height: 12),
                    ElevatedButton(onPressed: dismiss, child: const Text('Close')),
                  ],
                ),
              ),
            );
          default:
            return null;
        }
      },
      child: DeclarativeTabsScaffold(
        items: const [
          DeclarativeTabItem(
            tab: tabHome,
            item: BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          ),
          DeclarativeTabItem(
            tab: tabSettings,
            item: BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
          ),
        ],
        currentTab: _currentTab,
        onSelectTab: (t) => setState(() => _currentTab = t),
        pagesByTab: _pagesByTab,
        setPagesForTab: _setPages,
        buildPage: _buildTabPage,
        isOverlayVisible: overlay != null,
        dismissOverlay: _dismissCurrentOverlay,
        canPopTopForTab: (_) => overlay == null, // block swipe if overlay visible
        onBackAtRoot: widget.onExit,
      ),
    );
  }
}

class _TabRootPage extends StatelessWidget {
  final String title;
  final VoidCallback onOpenDialog;
  final VoidCallback onGoDetail;

  const _TabRootPage({
    required this.title,
    required this.onOpenDialog,
    required this.onGoDetail,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ElevatedButton(onPressed: onOpenDialog, child: const Text('Open dialog')),
            const SizedBox(height: 12),
            ElevatedButton(onPressed: onGoDetail, child: const Text('Go detail (same tab)')),
          ],
        ),
      ),
    );
  }
}
4
likes
160
points
65
downloads

Publisher

verified publisherfinitefield.org

Weekly Downloads

Declarative navigation helpers (Navigator 2.0 pages) and local overlays without Navigator.push/pop or showDialog.

Repository (GitHub)
View/report issues

Topics

#flutter #navigation #declarative #navigator-2 #tabs

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on declarative_nav