I want to have a bottom navigation bar in my app which behaves like followed:
- each tab should have it's own nested navigator, so that i can switch to a subroute while persisting the bottomnavbar
- when switching to another tab, I want to be able to get back to previous tab with the back button
- when I tap on tab 1 again, I don't want to instantiate the subroute again but I want to get back to it's last state .. for example if I was on route '/tab1/subRoute1' I want to land on this view again
I was able to achieve 1 & 2, but I am stuck on point 3. Here is my construct:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bottom NavBar Demo',
home: BottomNavigationBarController(),
);
}
}
class BottomNavigationBarController extends StatefulWidget {
BottomNavigationBarController({Key key}) : super(key: key);
@override
_BottomNavigationBarControllerState createState() =>
_BottomNavigationBarControllerState();
}
class _BottomNavigationBarControllerState
extends State<BottomNavigationBarController> {
int _selectedIndex = 0;
List<int> _history = [0];
GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
final List<BottomNavigationBarRootItem> bottomNavigationBarRootItems = [
BottomNavigationBarRootItem(
routeName: '/',
nestedNavigator: HomeNavigator(
navigatorKey: GlobalKey<NavigatorState>(),
),
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
),
BottomNavigationBarRootItem(
routeName: '/settings',
nestedNavigator: SettingsNavigator(
navigatorKey: GlobalKey<NavigatorState>(),
),
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Settings'),
),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
final nestedNavigatorState =
bottomNavigationBarRootItems[_selectedIndex]
.nestedNavigator
.navigatorKey
.currentState;
if (nestedNavigatorState.canPop()) {
nestedNavigatorState.pop();
return false;
} else if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Navigator(
key: _navigatorKey,
initialRoute: bottomNavigationBarRootItems.first.routeName,
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
builder = (BuildContext context) {
return bottomNavigationBarRootItems
.where((element) => element.routeName == settings.name)
.first
.nestedNavigator;
};
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
),
bottomNavigationBar: BottomNavigationBar(
items: bottomNavigationBarRootItems
.map((e) => e.bottomNavigationBarItem)
.toList(),
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
void _onItemTapped(int index) {
if (index == _selectedIndex) return;
setState(() {
_selectedIndex = index;
_history.add(index);
_navigatorKey.currentState
.pushNamed(bottomNavigationBarRootItems[_selectedIndex].routeName)
.then((_) {
_history.removeLast();
setState(() => _selectedIndex = _history.last);
});
});
}
}
class BottomNavigationBarRootItem {
final String routeName;
final NestedNavigator nestedNavigator;
final BottomNavigationBarItem bottomNavigationBarItem;
BottomNavigationBarRootItem({
@required this.routeName,
@required this.nestedNavigator,
@required this.bottomNavigationBarItem,
});
}
abstract class NestedNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
NestedNavigator({Key key, @required this.navigatorKey}) : super(key: key);
}
class HomeNavigator extends NestedNavigator {
HomeNavigator({Key key, @required GlobalKey<NavigatorState> navigatorKey})
: super(
key: key,
navigatorKey: navigatorKey,
);
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/home/1':
builder = (BuildContext context) => HomeSubPage();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
);
}
}
class SettingsNavigator extends NestedNavigator {
SettingsNavigator({Key key, @required GlobalKey<NavigatorState> navigatorKey})
: super(
key: key,
navigatorKey: GlobalKey<NavigatorState>(),
);
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (BuildContext context) => SettingsPage();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: RaisedButton(
onPressed: () => Navigator.of(context).pushNamed('/home/1'),
child: Text('Open Sub-Page'),
),
),
);
}
}
class HomeSubPage extends StatefulWidget {
const HomeSubPage({Key key}) : super(key: key);
@override
_HomeSubPageState createState() => _HomeSubPageState();
}
class _HomeSubPageState extends State<HomeSubPage> {
String _text;
@override
void initState() {
_text = 'Click me';
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Sub Page'),
),
body: Center(
child: RaisedButton(
onPressed: () => setState(() => _text = 'Clicked'),
child: Text(_text),
),
),
);
}
}
class SettingsPage extends StatelessWidget {
const SettingsPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings Page'),
),
body: Container(
child: Center(
child: Text('Settings Page'),
),
),
);
}
}
When you run this code, then tap "Open Sub-Page" -> tap "Click me" you should see "Clicked" in "Home Sub Page". if you now click on "Settings" in the bottom nav bar and then use the android back button you are back on "Home"-tab showing the exact same page where the button says "Clicked". if you click on "Settings" and then on "Home" in the bottom nav bar you are again on the exact same page where the button says "clicked". That is exactly the behaviour I need BUT when you do the latter you also get an error saying "Duplicate GlobalKey detected in widget tree.". And if you now tap the android back button twice you land on an empty page (out of obious reasons). How can I avoid this Duplicate Global Key Error without losing my desired behaviour?
I hope my explanation makes sense ..
An example app where this is implemented perfectly is Instagram.
This is related to: Flutter persistent navigation bar with named routes?