Last active
August 24, 2021 04:42
-
-
Save IsmailAlamKhan/c08d8517c503327f021a39c8786f713d to your computer and use it in GitHub Desktop.
Nested Navigation with Bottom navbar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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