Reputation: 618
I want to have a bottom navigation bar in my app which behaves like followed:
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?
Upvotes: 4
Views: 7512
Reputation: 1422
you want make navigation tabs just like twitter , Instagram, apps so every tab has it own navigation history and scoop
i guess i understand what you want achieve , but you do it in a wrong way
you should use 'tabBarView' for tabs content inside 'ScoopWillPop' and make every tab manage its own navigate history , after so many hard work on one of my projects i found the best way to implement this idea
i made many changes on your code , i hope to be clear
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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> with SingleTickerProviderStateMixin{
int _selectedIndex = 0;
List<int> _history = [0];
GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
TabController _tabController;
List<Widget> mainTabs;
List<BuildContext> navStack = [null, null]; // one buildContext for each tab to store history of navigation
@override
void initState() {
_tabController = TabController(vsync: this, length: 2);
mainTabs = <Widget>[
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[0] = context;
return HomePage();
});
}),
Navigator(
onGenerateRoute: (RouteSettings settings){
return PageRouteBuilder(pageBuilder: (context, animiX, animiY) { // use page PageRouteBuilder instead of 'PageRouteBuilder' to avoid material route animation
navStack[1] = context;
return SettingsPage();
});
}),
];
super.initState();
}
final List<BottomNavigationBarRootItem> bottomNavigationBarRootItems = [
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
),
BottomNavigationBarRootItem(
bottomNavigationBarItem: BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('Settings'),
),
),
];
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: TabBarView(
controller: _tabController,
physics: NeverScrollableScrollPhysics(),
children: mainTabs,
),
bottomNavigationBar: BottomNavigationBar(
items: bottomNavigationBarRootItems.map((e) => e.bottomNavigationBarItem).toList(),
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
),
onWillPop: () async{
if (Navigator.of(navStack[_tabController.index]).canPop()) {
Navigator.of(navStack[_tabController.index]).pop();
setState((){ _selectedIndex = _tabController.index; });
return false;
}else{
if(_tabController.index == 0){
setState((){ _selectedIndex = _tabController.index; });
SystemChannels.platform.invokeMethod('SystemNavigator.pop'); // close the app
return true;
}else{
_tabController.index = 0; // back to first tap if current tab history stack is empty
setState((){ _selectedIndex = _tabController.index; });
return false;
}
}
},
);
}
void _onItemTapped(int index) {
_tabController.index = index;
setState(() => _selectedIndex = index);
}
}
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 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).push(MaterialPageRoute(builder: (context) => HomeSubPage())),
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> with AutomaticKeepAliveClientMixin{
@override
// implement wantKeepAlive
bool get wantKeepAlive => true;
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),
),
),
);
}
}
/* convert it to statfull so i can use AutomaticKeepAliveClientMixin to avoid disposing tap */
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin{
@override
// implement wantKeepAlive
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings Page'),
),
body: Container(
child: Center(
child: Text('Settings Page'),
),
),
);
}
}
Upvotes: 13