Skip to content

Instantly share code, notes, and snippets.

@IsmailAlamKhan
Last active August 24, 2021 04:42
Show Gist options
  • Save IsmailAlamKhan/c08d8517c503327f021a39c8786f713d to your computer and use it in GitHub Desktop.
Save IsmailAlamKhan/c08d8517c503327f021a39c8786f713d to your computer and use it in GitHub Desktop.
Nested Navigation with Bottom navbar
import 'package:flutter/material.dart';
import 'dart:developer';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: BottomNav(),
// home: MyHomePage(),
);
}
}
WidgetBuilder _page(String title) => (context) => Scaffold(
body: Center(
child: Text(
title,
style: TextStyle(fontSize: 20, color: Colors.grey),
),
),
);
class BottomNav extends StatefulWidget {
const BottomNav({Key? key}) : super(key: key);
@override
_BottomNavState createState() => _BottomNavState();
}
class _BottomNavState extends State<BottomNav> {
late final BottomNavNotifier _bottomNavNotifier;
final pageList = [
BottomNavModel(
icon: Icons.home,
title: 'Home',
page: _page('Home'),
),
BottomNavModel(
icon: Icons.ac_unit,
title: 'AC',
page: _page('AC'),
),
BottomNavModel(
icon: Icons.person,
title: 'Profile',
page: _page('Profile'),
),
BottomNavModel(
icon: Icons.settings,
title: 'Settings',
page: _page('Settings'),
),
];
@override
void initState() {
super.initState();
_bottomNavNotifier = BottomNavNotifier(pageList);
}
@override
void dispose() {
_bottomNavNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _BottomNavNotifierScope(
notifier: _bottomNavNotifier,
child: Builder(builder: (context) {
final value = BottomNavNotifier.of(context).notifier!;
return WillPopScope(
onWillPop: value.onWillPop,
child: Scaffold(
appBar: AppBar(
leading: value.canPop
? BackButton(
onPressed: () {
value.navState?.pop();
value.update();
},
)
: const SizedBox.shrink(),
),
body: _body(),
bottomNavigationBar: _bottomNavbar(value),
),
);
}),
);
}
Widget _bottomNavbar(BottomNavNotifier value) {
return BottomNavigationBar(
backgroundColor:
ThemeData.dark().bottomNavigationBarTheme.backgroundColor,
type: BottomNavigationBarType.fixed,
currentIndex: value.currentIndex,
onTap: value.bottomNavOnTap,
items: pageList
.map(
(e) => BottomNavigationBarItem(
icon: Icon(e.icon),
label: e.title,
),
)
.toList(),
);
}
Navigator _body() {
return Navigator(
key: _bottomNavNotifier.mainNavKey,
observers: [
BottomNavObserver(
name: 'Main nested nav',
onPop: _bottomNavNotifier.onPop,
)
],
initialRoute: pageList[0].url,
onGenerateRoute: _bottomNavNotifier.onGenerateRoute,
);
}
}
class BottomNavNotifier extends ChangeNotifier {
Map<String, BottomNavModel> get _urls => {
for (var page in pageList) page.url: page,
};
final List<BottomNavModel> pageList;
BottomNavNotifier(this.pageList);
BottomNavModel get activeModel => pageList[_currentIndex];
NavigatorState? get activeNestedNavState => activeModel.navKey.currentState;
bool get nestedNavCanPop => activeNestedNavState?.canPop() ?? false;
final GlobalKey<NavigatorState> mainNavKey = GlobalKey<NavigatorState>();
bool get canPop =>
(mainNavKey.currentState?.canPop() ?? false) ||
(activeNestedNavState?.canPop() ?? false);
NavigatorState? get navState =>
nestedNavCanPop ? activeNestedNavState : mainNavKey.currentState;
int _currentIndex = 0;
int get currentIndex => _currentIndex;
set currentIndex(int currentIndex) {
_currentIndex = currentIndex;
notifyListeners();
}
void update() => notifyListeners();
Future<bool> onWillPop() async {
if (nestedNavCanPop) {
notifyListeners();
}
if (canPop) {
navState?.pop();
return false;
}
return true;
}
void bottomNavOnTap(int index) {
if (currentIndex != index) {
currentIndex = index;
mainNavKey.currentState?.pushNamed(pageList[currentIndex].url);
}
}
void onPop(Route? newRoute, Route? oldRoute) {
final _dict = _urls;
final url = oldRoute?.settings.name;
if (_dict.containsKey(url)) {
currentIndex = pageList.indexOf(_dict[url]!);
}
}
Route? onGenerateRoute(RouteSettings settings) {
final currentUrl = settings.name;
final _model = _urls[currentUrl];
if (_model != null) {
return MaterialPageRoute(
builder: _model.page,
settings: settings,
);
}
}
static _BottomNavNotifierScope of(BuildContext context) {
try {
return context
.dependOnInheritedWidgetOfExactType<_BottomNavNotifierScope>()!;
} catch (e) {
throw FlutterError('No _BottomNavNotifierScope found on scope');
}
}
}
class BottomNavModel {
final String title, url;
final IconData icon;
final WidgetBuilder page;
final GlobalKey<NavigatorState> navKey;
BottomNavModel({
required this.title,
required this.icon,
required this.page,
}) : url = title.toLowerCase(),
navKey = GlobalKey<NavigatorState>();
}
typedef BottomNavbarBuilder = Widget Function(BottomNavNotifier notifier);
typedef BodyBuilder = Widget Function(
BottomNavNotifier notifier,
Widget bottomNavbar,
Widget body,
);
typedef OnGenerateRoute = Route Function(
RouteSettings settings,
BottomNavModel? acitveModel,
);
class BottomNavObserver extends NavigatorObserver {
final Function(Route? route, Route? oldRoute)? onPop;
final String name;
BottomNavObserver({this.name = 'Bottom Navbar', this.onPop});
void _logger(String msg) => log(msg, name: name);
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
_logger(
'Going from ${previousRoute?.settings.name} to new route ${route.settings.name}',
);
}
@override
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
_logger(
'Popping from ${route.settings.name} and going to ${previousRoute?.settings.name}',
);
onPop?.call(route, previousRoute);
}
}
class _BottomNavNotifierScope extends InheritedNotifier<BottomNavNotifier> {
const _BottomNavNotifierScope({
Key? key,
required Widget child,
required BottomNavNotifier notifier,
}) : super(
child: child,
notifier: notifier,
key: key,
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment