I want to have persistent bottom navigation bar across my whole app but exclude bottom navigation bar in some routes like login page.
I created BottomNavigationBar widget:
class MyBottomNavigationBar extends StatefulWidget {
final int bottomIndex;
const MyBottomNavigationBar({Key key, this.bottomIndex}) :
super(key: key);
State createState() => _MyBottomNavigationBarState();
}
class _MyBottomNavigationBarState extends State
<MyBottomNavigationBar> {
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(LineIcons.film),
title: Text(
'1',
),
),
BottomNavigationBarItem(
icon: Icon(LineIcons.ticket),
title: Text(
'2',
),
),
BottomNavigationBarItem(
icon: Icon(LineIcons.user),
title: Text(
'3',
),
),
],
currentIndex: widget.bottomIndex,
onTap: (int index) {
setState(() {
switch (index) {
case 0 :
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomePage()));
break;
case 1:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MyTickets()));
break;
case 2:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainProfile()));
break;
}
});
}
);
}
}
Then in build() of each page where i want to create BottomNavigationBar I write:
bottomNavigationBar: MyBottomNavigationBar(bottomIndex: 0,)
or
bottomNavigationBar: MyBottomNavigationBar(bottomIndex: 1,),
or
bottomNavigationBar: MyBottomNavigationBar(bottomIndex: 2,),
Everything is ok, but I have a problem: each time when I open ANY page with bottomNavigationBar, my main page (HomePage()) is rebuild and call methods from api. How can I avoid it? Thank you
Maybe you can implement this using the indexed stack. Just check this link in which you might get the desired output.
Flutter: BottomNavigationBar rebuilds Page on change of tab
Related
On one of the pages, nested inside one of bottom navigation bar pages I want to hide the bottom navigation bar, which is set as global. To be clear, I'm talking about this bar:
I can't just use Navigator.pushNamed because I'm creating viewModel and passing arguments in this way:
openConversation(BuildContext context) async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (context) => ConversationVM(...),
child: ConversationScreen(
(...)
),
),
));
}
I've tried to set the Scaffold parameter bottomNavigationBar to null, but without effect, I need to resolve that problem somewhere higher.
Nav bar snippet:
class NavigationBar extends StatefulWidget {
static String id = 'navigation_screen';
#override
State<StatefulWidget> createState() {
return _NavigationBarState();
}
}
class _NavigationBarState extends State<NavigationBar> {
//track the index of our currently selected tab
int _currentIndex = 0;
//st of widgets that we want to render
final List<Object> _children = [
PostedQueries(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
//body of our scaffold which is the widget that gets displayed between our app bar and bottom navigation bar.
body: _children[_currentIndex], // new
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
// new
currentIndex: _currentIndex,
// new
unselectedItemColor: Colors.grey,
selectedItemColor: Colors.deepOrange,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home),
label: ('Home'),
),
],
),
);
}
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
I work with provider and I am a little confused with a problem. I want to simulate an action on the navigation bar when I click a button on the page.
Example:
My navigation bar consists of 3 items (menu 1, menu 2, menu 3). By clicking on the "Go to menu 2" button on my page, I want to make it look like I was clicking on the navigation bar. So the page changes and the navigation bar is updated.
In my example I will work with 3 pages to make it easier to understand. InitialElementScreen, InitialElementResultScreen, CorrectionsScreen.
The InitialElementResultScreen has no navigation bar.
My problem:
For that I use provider. I manage to do my system when I am on a menu page. Example if I put my button on the InitialElementScreen page then my system will work and take me to CorrectionsScreen.
It no longer works if my button is on the InitialElementResultScreen page which is a page opened with navigator.push from InitialElementScreen.
Here is my tree structure when I am on InitialElementScreen:
My AppScreen:
class AppScreen extends StatelessWidget {
const AppScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LicenseNotifier>(
create: (_) => LicenseNotifier(),
child: MaterialApp(
title: AppConfig.APPLICATION_NAME,
debugShowCheckedModeBanner: false,
theme: AppTheme().data,
initialRoute: AppRoutes.HOME,
onGenerateRoute: RoutesClass.generate,
),
);
}
}
My RouteClass :
abstract class RoutesClass{
static Route<dynamic> generate(RouteSettings settings){
final args = settings.arguments;
switch(settings.name){
case AppRoutes.HOME :
return MaterialPageRoute(
builder: (context) => Consumer(
builder: (context, LicenseNotifier license, _) {
return license.selectHomeScreen();
},
),
);
break;
}
My Widget selectHomeScreen in my LicenseNotifier:
Widget selectHomeScreen()
{
switch (appState) {
case AppState.Uninitialized:
return SplashedScreen();
break;
case AppState.Authenticating:
case AppState.Unauthenticated:
return LicenseHomeScreen(title: "Activate the license");
break;
case AppState.Authenticated:
return ChangeNotifierProvider<BottomNavigationBarNotifier>(
create: (BuildContext context) => BottomNavigationBarNotifier(),
child: NavigationBarScreen(),
);
break;
}
return null;
}
My NavigationBarScreen :
class NavigationBarScreen extends StatelessWidget
{
NavigationBarScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
var navigationProvider = Provider.of<BottomNavigationBarNotifier>(context);
return SafeArea(
child: Scaffold(
body: navigationProvider.loadScreenWithNavigation(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationProvider.currentIndex,
onTap: (index) {
navigationProvider.navigationScreenIndex = index;
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Menu 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.calculate),
label: 'Menu 2',
),
BottomNavigationBarItem(
icon: Icon(Icons.library_books_sharp),
label: 'Menu 3',
),
],
),
),
);
}
}
My BottomNavigationBarNotifier:
class BottomNavigationBarNotifier with ChangeNotifier {
int currentIndex = AppConfig.NAVIGATION_DEFAULT_INDEX;
BottomNavigationBarNotifier();
set navigationScreenIndex(int index) {
currentIndex = index;
notifyListeners();
}
Widget loadScreenWithNavigation()
{
switch (currentIndex)
{
case 0:
return MultiProvider(
providers: [
... some providers
],
child: InitialElementScreen(title : 'Menu 1'),
);
break;
case 1:
return CorrectionsScreen(title: 'Menu 2')
break;
case 2:
return TableHomeScreen(title: 'Menu 3');
break;
default:
return InitialElementScreen(title : 'Menu 1');
break;
}
}
}
So if the button "Go to Menu 2" is in my InitialElementScreen, it's works. The code of the button:
var _formProvider = Provider.of<BottomNavigationBarNotifier>(context, listen: false);
_formProvider.navigationScreenIndex = 1;
notifyListeners();
Now I want my "Go to Menu 2" button to be in a sub page of menu 1, so I'm going to have a button in my InitialElementScreen to open my InitialElementResultScreen page:
await Navigator.pushNamed(context, AppRoutes.RESULT, arguments: jsonEncode(data.toMap()));
Now still in my RouteClass file, I add this code to load my InitialElementResultScreen page:
case AppRoutes.RESULT:
var data = settings.arguments as String;
return MaterialPageRoute(
builder: (context) => Consumer(
builder: (context, LicenseNotifier license, _) {
return ChangeNotifierProvider<BottomNavigationBarNotifier>(
create: (BuildContext context) => BottomNavigationBarNotifier(),
child: ChangeNotifierProvider<InitialCalculatorResultNotifier>(
create: (BuildContext context) => InitialCalculatorResultNotifier(),
child: InitialElementResultScreen(title: "Sub Menu 1", data: data),
),
);
},
),
);
break;
Here is now my tree structure when I am on my sub page of menu 1, InitialElementResultScreen:
If I put my "Go to Menu 2" button back in the InitialElementResultScreen page, it doesn't work anymore ... I come back to the Menu 1 page, InitialElementScreen, why ?
Code of my button "Go to Menu 2"
which is the same as shown above with a Navigator.pop:
var _formProvider = Provider.of<BottomNavigationBarNotifier>(context, listen: false);
_formProvider.navigationScreenIndex = 1;
notifyListeners();
Navigator.pop(context);
EDIT: I resolve my problem !
From what I understand the secondary page opened with navigator.push opens under the MaterialApp Widget. So I moved my BottomNavigationBarNotifier above.
class AppScreen extends StatelessWidget {
const AppScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LicenseNotifier>(
create: (_) => LicenseNotifier(),
child: ChangeNotifierProvider<BottomNavigationBarNotifier>(
create: (BuildContext context) => BottomNavigationBarNotifier(),
child: MaterialApp(
title: AppConfig.APPLICATION_NAME,
debugShowCheckedModeBanner: false,
theme: AppTheme().data,
initialRoute: AppRoutes.HOME,
onGenerateRoute: RoutesClass.generate,
),
)
);
}
}
I am building an app with a bottom appbar with classic indexed pages to navigate the main menu:
class OverAllScreen extends StatefulWidget {
final int initialPage;
OverAllScreen(this.initialPage);
#override
_OverAllScreenState createState() => _OverAllScreenState();
}
class _OverAllScreenState extends State<OverAllScreen> {
List _pageOptions = [
Shop(),
Home(),
Discover(),
Account(),
];
int _page;
#override
void initState() {
super.initState();
setState(() {
_page = widget.initialPage;
});
}
#override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _mediaSize = MediaQuery.of(context).size;
return Scaffold(
body: _pageOptions[_page],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: _theme.primaryColor,
selectedItemColor: _theme.accentColor,
unselectedItemColor: Colors.white,
currentIndex: _page,
onTap: (index) {
setState(() {
_page = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'),
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Discover'),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline), label: 'Account'),
],
),
);
}
}
In the account page, further down the widget tree I have a widget that shows a list of detailed products.
I want this page to open up when I click on an item of a simple (non-detailed) grid of products.
This I can easily do with Navigator.of(context).push(MaterialPageR0ute(...))). However I would like to keep the bottomAppBAr visible (like instagram when you look at the products of a user).
The problem is that I have the specific list of products in down in the widget tree, so I can't pass them as an argument at the occount page level, without passing them back each step of the way by passing a function as an argument of the widget.
class ProductList extends StatelessWidget {
#override
Widget build(BuildContext context) {
var _products = Provider.of<List<ProductModel>>(context);
return ListView.builder(
itemExtent: 150,
scrollDirection: Axis.horizontal,
itemCount: _products.length,
itemBuilder: (context, index) => Card(
margin: const EdgeInsets.all(5),
child: InkWell(
child: ProductTile(_products[index]),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProductDetailList(
productIndex: index,
products: _products,
),
),
),
),
),
);
}
}
Here I use MaterialPageRoute, but would like to keep BottomAppBar visible.
Thank you.
To achieve the functionality you are looking for, you would have to write several switch and break case which can be cumbersome as your use is going to be very basic.
What I would suggest is go for this package
This package provide a persistent bottom navigaton bar which is highly customisable and you could even use salmon like bottom bar without writing a single line of extra code.
I have asked similar question here and based on the feedback I got, have tried few approaches, but couldn't get it working, as the original question was little old and already closed, I am posting with my new findings.
Ideally, this is what I am trying to achieve: If the Flutter Bottomsheet is open, I would like to keep it open and let the app go to background when the 'back' button is pushed, i.e. when the app is bought back I have Bottomsheet in view as is.
Have a MyApp with a root NavigationKey to start with and it opens (on default route) the RealApp with its own Key, Bottomsheet, Tabs etc. If any Tabs are pushed, clicking the 'Back' button will Pop those views. And if there aren't any more views to Pop, the default behavior of Flutter is to Pop the BottomNavigation which I am trying to override and instead want the app to go to background as is.
I tried different options including Poping the Root key from onWillPop without much Success when there are no more views to Pop.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: rootGlobalKey,
home: RealApp()
);
}
}
class RealApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final pagesRouteFactories = {
"/": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"HomePage",
style: Theme.of(context).textTheme.body1,
),
),
),
"takeOff": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Take Off",
style: Theme.of(context).textTheme.body1,
),
),
),
"landing": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Landing",
style: Theme.of(context).textTheme.body1,
),
),
),
"settings": () => MaterialPageRoute(
builder: (context) => Center(
child: Text(
"Settings",
style: Theme.of(context).textTheme.body1,
),
),
),
};
final RealBottomSheet bottomSheet = new RealBottomSheet();
#override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
key: _scaffoldKey,
body: _buildBody(context),
bottomNavigationBar: _buildBottomNavigationBar(context),
),
);
Widget _buildBody(context) => WillPopScope(
onWillPop: () async {
if(navigatorKey.currentState.canPop()) {
// Navigator.pop(context);
navigatorKey.currentState.pop();
return false;
}else {
// Returning true will remove BottomSheet from view, followed by moving the app to background state
// Need a way where the BottomSheet is kept open while the app can go to background state
// Navigator.of(context, rootNavigator: true).pop();
rootGlobalKey.currentState.pop();
// SystemChannels.platform.invokeMethod('SystemNavigator.pop');
return false;
}
},
child: MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: (route) => pagesRouteFactories[route.name]())
);
Widget _buildBottomNavigationBar(context) => BottomNavigationBar(
items: [
_buildBottomNavigationBarItem("Home", Icons.home),
_buildBottomNavigationBarItem("Take Off", Icons.flight_takeoff),
_buildBottomNavigationBarItem("Landing", Icons.flight_land),
_buildBottomNavigationBarItem("Settings", Icons.settings)
],
onTap: (routeIndex) {
if (routeIndex == 0) return routeToView(routeIndex);
if (routeIndex == 1) return routeToView(routeIndex);
if (routeIndex == 2) return routeToView(routeIndex);
if (routeIndex == 3) return _showBottomSheet();
});
_buildBottomNavigationBarItem(name, icon) => BottomNavigationBarItem(
icon: Icon(icon), title: Text(name), backgroundColor: Colors.black45);
void routeToView(routeIndex) {
navigatorKey.currentState.pushNamed(pagesRouteFactories.keys.toList()[routeIndex]);
}
void _showBottomSheet() {
_scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
return _buildBottomSheet(context);
});
}
Widget _buildBottomSheet(BuildContext context) {
return bottomSheet;
}
}
I'm trying to setup a multipage app in Flutter, where the bottom tabbar contains the four most used pages, including a "More" button. The more button shows a page with links to other pages. When clicking on a page, I want this page to replace the "More" page, so a new navigation stack is created. However, when switching to another tab and then back to the "more" tab, the navigation state is remembered. This means I'm not seeing the "More" page, but the page I left when switching tabs. I want to show the "More" page everytime the user clicks on the "More" tab.
I've tried using the pushReplacement method of the Navigator, so when the user clicks on a more-page, he can't navigate back to the list of more pages. However, when switching tabs, the more page is now never shown, because it's replaced.
I've also tried adjusting the tab callback method, to pop all views and return the navigator to the MoreScreen. However, this left me with an error in the console: setState() or markNeedsBuild() called during build..
The tab screen:
class TabScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return TabScreenState();
}
}
class TabScreenState extends State<TabScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => Future<bool>.value(true),
child: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Artikelen'),
),
BottomNavigationBarItem(
icon: Icon(Icons.message),
title: Text('Activiteiten'),
),
BottomNavigationBarItem(
icon: Icon(Icons.speaker_group),
title: Text('Training'),
),
BottomNavigationBarItem(
icon: Icon(Icons.more),
title: Text('Meer'),
),
],
),
tabBuilder: (BuildContext context, int index) {
assert(index >= 0 && index <= 3);
switch (index) {
case 0:
return CupertinoTabView(
builder: (BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
return ArticleListScreen();
},
defaultTitle: 'Artikelen',
);
break;
case 1:
return CupertinoTabView(
builder: (BuildContext context) => ActivityListScreen(),
defaultTitle: 'Activiteiten',
);
break;
case 2:
return CupertinoTabView(
builder: (BuildContext context) => ArticleListScreen(),
defaultTitle: 'Training',
);
break;
case 3:
return CupertinoTabView(
builder: (BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
return MoreScreen();
},
defaultTitle: 'Meer',
);
break;
}
return null;
},
),
),
);
}
}
And the MoreScreen which holds a button to go to another page:
class MoreScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MoreScreenState();
}
}
class MoreScreenState extends State<MoreScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: ThemeData.dark().accentColor,
alignment: AlignmentDirectional(0.0, 0.0),
child: MaterialButton(
child: Text('PUSH page'),
onPressed: () => {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => ArticleListScreen()))
},
)
),
);
}
}
I expect the navigation stack to reset everytime I switch tabs, but now I have to do that manually using:
Navigator.of(context).popUntil((route) => route.isFirst);
This leads to the error message: setState() or markNeedsBuild() called during build. after pushing a new page in a tab and then switching tabs.