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.
Related
I want to close my Drawer widget every time the user presses a button in the Bottom Navigation Bar, but can't quite figure this thing out. The way my setup of the BNB is set now is that the current state of all screen is remembered through out the app (using an IndexedStack), but I want to close the Drawer if it is opened in any of the screens before the BNB button press. Each of my screens have their own Drawers and AppBars, so I can't make one Drawer inside the BNB (or I can and I can dynamically change them with a switch case when a specific Screen is clicked on BUT then the Drawer will cover the Bottom Navigation Bar etc.), but I want to make it work like this for now. So here is the code with some comments inside to explain things:
Bottom Navigation Bar:
class BottomNavBar extends StatefulWidget {
static const String id = 'bottom_navbar_screen';
#override
_BottomNavBarState createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _selectedIndex = 0;
/// list of screen that will render inside the BNB
List<Navigation> _items = [
Navigation(
widget: Screen1(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen2(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen3(), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen4(), navigationKey: GlobalKey<NavigatorState>()),
];
/// function that renders components based on selected one in the BNB
void _onItemTapped(int index) {
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
/// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
Widget _navigationTab(
{GlobalKey<NavigatorState> navigationKey, Widget widget}) {
return Navigator(
key: navigationKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(builder: (context) => widget);
},
);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final isFirstRouteInCurrentTab =
!await _items[_selectedIndex].navigationKey.currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_selectedIndex != 0) {
_onItemTapped(1);
return false;
}
}
/// let system handle back button if we're on the first route
return isFirstRouteInCurrentTab;
},
child: Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _items
.map((e) => _navigationTab(
navigationKey: e.navigationKey, widget: e.widget))
.toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Screen 1,
),
BottomNavigationBarItem(
label: 'Screen 2,
),
BottomNavigationBarItem(
label: 'Screen 3,
),
BottomNavigationBarItem(
label: 'Screen 4,
),
],
currentIndex: _selectedIndex,
showUnselectedLabels: true,
onTap: _onItemTapped,
),
),
);
}
}
Let's say all 4 screens are the same and they have their own AppBar and Drawer:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4'),
),
body: Text('Body of Screens 1-4'),
);
}
So because each of the screens have their own AppBars and Drawers, the Drawer doesn't render over the Bottom Navigation Bar, so my BNB buttons can be clicked. If I put one Drawer for all Screens inside the BNB, then you can't click the BNB unless you close the Drawer first, which is not something I'm looking for right now.
So, my final question is, how do I close each of the Screens Drawers (if they are previously opened that is) when you press the BottomnavigationBar? (i.e. I am on Screen 1, I open the Drawer, then I press on Screen 2 in the BNB and I want to pop()/close the Drawer in Screen 1 before I navigate to Screen 2.)
Thanks in advance for your help!
A good way to do this is to use a GlobalKey for your scaffold.
So, for all your scaffolds, you define them using:
class SomeClass extends StatelessWidget {
final scaffoldKey = GlobalKey<ScaffoldState>()
Widget build(BuildContext context) {
Scaffold(
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4),
),
body: Text('Body of Screens 1-4),
key: scaffoldKey,
),
);
}
}
And then, you can pass this key to your BottomNavigationBar.
In your BottomNavigationBar, you can have all the scaffoldKeys, and in the onItemTap function:
void _onItemTapped(int index) {
for (scaffoldKey in scaffoldKeys) {
// If the drawer is open
if (scaffoldKey.currentState.isDrawerOpen) {
// Closes the drawer
scaffoldKey.currentState?.openEndDrawer();
}
}
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
It's up to you to find the best way of passing around the keys. You could for example define them in a Widgets that contains both the bottom navigation bar and the different scaffolds, and pass it down as parameters. You could use State Management... whatever fits your use case.
Here is what your code could look like:
class BottomNavBar extends StatefulWidget {
static const String id = 'bottom_navbar_screen';
#override
_BottomNavBarState createState() => _BottomNavBarState();
}
class _BottomNavBarState extends State<BottomNavBar> {
int _selectedIndex = 0;
late final List<GlobalKey<ScaffoldState>> scaffoldKeys;
/// list of screen that will render inside the BNB
late final List<Navigation> _items;
#override
initState() {
super.initState()
scaffoldKeys = [GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>(), GlobalKey<ScaffoldState>()];
_items = [
Navigation(
widget: Screen1(scaffoldKey: scaffoldKeys[0]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen2(scaffoldKey: scaffoldKeys[1]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen3(scaffoldKey: scaffoldKeys[2]), navigationKey: GlobalKey<NavigatorState>()),
Navigation(
widget: Screen4(scaffoldKey: scaffoldKeys[3]), navigationKey: GlobalKey<NavigatorState>()),
];
}
/// function that renders components based on selected one in the BNB
void _onItemTapped(int index) {
for (scaffoldKey in scaffoldKeys) {
// If the drawer is open
if (scaffoldKey.currentState.isDrawerOpen) {
// Closes the drawer
scaffoldKey.currentState?.openEndDrawer();
}
}
if (index == _selectedIndex) {
_items[index]
.navigationKey
.currentState
.popUntil((route) => route.isFirst);
} else {
setState(() {
_selectedIndex = index;
});
}
/// when the index is selected, on the button press do some actions
switch (_selectedIndex) {
case 0:
// Do some actions
break;
case 1:
// Do some actions
break;
case 2:
// Do some actions
break;
case 3:
// Do some actions
break;
}
}
/// navigation Tab widget for a list of all the screens and puts them in a Indexed Stack
Widget _navigationTab(
{GlobalKey<NavigatorState> navigationKey, Widget widget, GlobalKey<ScaffoldState> scaffoldKey}) {
return Navigator(
key: navigationKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(builder: (context) => widget);
},
);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final isFirstRouteInCurrentTab =
!await _items[_selectedIndex].navigationKey.currentState.maybePop();
if (isFirstRouteInCurrentTab) {
if (_selectedIndex != 0) {
_onItemTapped(1);
return false;
}
}
/// let system handle back button if we're on the first route
return isFirstRouteInCurrentTab;
},
child: Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _items
.map((e) => _navigationTab(
navigationKey: e.navigationKey, widget: e.widget))
.toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'Screen 1,
),
BottomNavigationBarItem(
label: 'Screen 2,
),
BottomNavigationBarItem(
label: 'Screen 3,
),
BottomNavigationBarItem(
label: 'Screen 4,
),
],
currentIndex: _selectedIndex,
showUnselectedLabels: true,
onTap: _onItemTapped,
),
),
);
}
}
And you screens:
class Screen1 extends StatelessWidget {
final GlobalKey<ScaffoldState> scaffoldKey;
Screen1({required this.scaffoldKey});
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
backgroundColor: Colors.white,
drawer: Drawer(), // so this is what I want to close on BNB button press in each of the 4 screens
appBar: AppBar( // each screen has its own app bar
title: Text('Screens 1-4'),
),
body: Text('Body of Screens 1-4'),
);
}
}
I changed the list of screens _items to a late variables so you can pass the scaffoldKeys to them when declaring them.
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 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
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;
}
}
Hello I am new to Flutter and I am trying to implement a bottom tab bar with multiple navigation screen for each tab.
Here is my initial set up
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return CupertinoApp(home: HomeScreen(),
routes: {
Screen1.id: (context) => Screen1(),
Screen2.id: (context) => Screen1(),
DetailScreen3.id: (context) => DetailScreen3(),
DetailScreen4.id: (context) => DetailScreen4(),
});
}
}
Here is my HomeScreen
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.book_solid),
title: Text('Articles'),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.eye_solid),
title: Text('Views'),
),
],
),
tabBuilder: (context, index) {
if (index == 0) {
return Screen1();
} else {
return Screen2();
}
},
);
}
}
Here is my screen1
class Screen1 extends StatelessWidget {
static const String id = 'screen1';
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, DetailScreen3.id);
},
child: Center(
child: Text('Screen 1',),
),
),
);
}
}
and here is my screen3
class DetailScreen3 extends StatelessWidget {
static const String id = 'screen3';
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Center(
child: Text('terzo schermo',),
),
);
}
}
The tabBar work ok and I am able to swap between the 2 tabs but I am not able to navigate from screen 1 to screen 3. When I tap on screen1 Center widget, the screen start to navigate but half way it stops and then the screen become all black...
Here is the error
There are multiple heroes that share the same tag within a subtree.
Within each subtree for which heroes are to be animated (i.e. a
PageRoute subtree), each Hero must have a unique non-null tag. In this
case, multiple heroes had the following tag: Default Hero tag for
Cupertino navigation bars with navigator NavigatorState#05492(tickers:
tracking 2 tickers)
I understand the problem is related to the hero tag of the navigation bar which must have a unique identifier. How should I fix this problem? should I assign an heroTag to all navigation bar???
Many thanks in advance for the help
I resolved by setting the following properties for each CupertinoNavigationBar
heroTag: 'screen1', // a different string for each navigationBar
transitionBetweenRoutes: false,
As an iOS developer, I tried flutter for the first time, this thing caused a black screen after jumping the page, and also troubled me for two days
heroTag: 'screen1', // a different string for each navigationBar
transitionBetweenRoutes: false,