declarative_nav 0.0.1
declarative_nav: ^0.0.1 copied to clipboard
Declarative navigation helpers (Navigator 2.0 pages) and local overlays without Navigator.push/pop or showDialog.
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)')),
],
),
),
);
}
}