Best approach to Android navigation back button on Nested Navigator - flutter

I have a Scaffold that holds a Navigator object. Each route has a possibility of holding an appBar.
MainScaffold
Scaffold(
appBar: AppBarSelector(),
...
body: WillPopScope(...},
child: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
Widget builder;
switch (settings.name) {
case '/':
ref.read(routeNameProvider.state).state = '/';
builder = HomeScreenBody(
automotive: content["stuff"],
vehicleTypes: content["more_stuff"]["types"],
);
break;
case '/users':
ref.read(routeNameProvider.state).state = '/users';
builder = UsersScreenBody(
stuff: content["stuff"],
moreStuff: content["more_stuff"],
);
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return PageRouteBuilder( ...
pageBuilder: (BuildContext context, _animation, __animation) => builder,
...
bottomNavigationBar: CustomBottomNavigationBar(navigatorKey: _navigatorKey),
);
AppBarSelector
class AppBarSelector extends AppBarIcon {
#override
Widget build(BuildContext context, WidgetRef ref) {
return selectAppBar(ref);
}
Widget selectAppBar(WidgetRef ref) {
String routeName = ref.watch(routeNameProvider);
switch (routeName) {
case '/garage':
return AppBarIcon(
titleText: "Garage",
icon: Icons.add,
route: VehicleRegistrationScreen(),
);
break;
case '/home/list':
return AppBarIcon(
titleText: ref.read(appBarTitleProvider),
icon: Icons.add,
route: SomeList(),
);
break;
default:
return SizedBox(height: 0);
}
}
Problem
Whenever I route to a page, the appBar shows/dissapears just fine. But once I press the Android Navigation BackButton there is no routing happening, thus the appBar does not change how it should be and remain after popping the route on a screen that should not have an appBar.
Question
How do I make AppBarSelector() respond to my Android Navigation BackButton?
Approach made
I have tried the persistent_bottom_nav_bar 4.0.2 package but that lakcs things like a persistent appBar or the possibility to have a transparant BottomNavigationBar.
I wonder how other people have dealt with this situation.

Related

Flutter MaterialPageRoute as fullscreenDialog appears underneath BottomNavigationBar

There's a similar unanswered question here: FullscreenDialog screen not covering bottomnavigationbar
but I want to provide more context, to see if that helps find a solution.
We'll start at the top with my main.dart, I am building a MaterialApp that builds a custom DynamicApp. Here's the important stuff:
Widget build(BuildContext context) {
var _rootScreenSwitcher = RootScreenSwitcher(key: switcherKey);
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
builder: (context, child) {
return DynamicApp(
navigator: locator<NavigationService>().navigatorKey,
child: child,
switcher: _rootScreenSwitcher,
);
},
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
},
home: _rootScreenSwitcher,
);
}
My DynamicApp sets up the root Scaffold like so:
Widget build(BuildContext context) {
return Scaffold(
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
drawerItems: widget.drawerItems,
headerView: Container(
child: Text('Drawer Header'),
decoration: BoxDecoration(color: Colors.blue),
),
onNavigationItemSelect: (index) {
onTapped(index);
return true; // true means that drawer must close and false is Vice versa
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
onTapped(index);
},
currentIndex: _selectedIndex,
items: bottomNavBarItems,
showUnselectedLabels: false,
),
body: widget.child,
);
}
The child of the DynamicApp is a widget called RootScreenSwitcher which is an IndexedStack and controls the switching of screens from my BottomNavigationBar and also when items are selected in the Drawer. Here's the RootScreenSwitcher:
class RootScreenSwitcherState extends State<RootScreenSwitcher> {
int _currentIndex = 0;
int get currentIndex => _currentIndex;
set currentIndex(index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentIndex,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
),
);
}
void switchScreen(int index) {
setState(() {
_currentIndex = index;
});
}
}
Each of the main section of the app has its own Navigator and root screen. This all works fine, and I'm happy with the overall navigation structure. Each root screen has its own AppBar but the global Drawer and BottomNavigationBar are handled in the DynamicApp so I don't have to keep setting them in the other Scaffold screens.
So, then it came to start to introduce other sections of the app that are not serviced by the bottom tab bar, and can be presented from the Drawer or from other action buttons. Each of these new sections would have to be modal fullscreenDialog screens so they slide up from the bottom, but have their own navigation and scaffold.
My issue is that when I navigate to my SettingsNavigator screen it slides up from behind the BottomNavigationBar, and not on top of everything. Here's the onGenerateRoute method of the MaterialApp:
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case SettingsNavigator.routeName:
return MaterialPageRoute(
builder: (context) => SettingsNavigator(),
fullscreenDialog: true);
default:
return MaterialPageRoute(builder: (context) => SettingsNavigator());
}
}
I'm new to Flutter and don't quite understand how routing works with contexts, so I am wondering whether the context of the screen that calls the navigateTo method of the Navigator becomes the main build context, and is therefore not on top of the widget tree?
gif here: https://www.dropbox.com/s/nwgzo0q28cqk61p/FlutteRModalProb.gif?dl=0
Here's the tree structure that shows that the Scaffold for the Settings screen has been placed inside the DynamicApp Scaffold. The modal needs to sit above DynamicApp.
Can anyone shed some light on this?
UPDATE: I have tried creating and sharing a unique ScaffoldState key for the tab bar screens, and then the Settings page has a different key. It made no difference. I wonder now if it is the BuildContext having the same parent.
UPDATE UPDATE:
I had a breakthrough last night which has made me realise that it just isn't going to be possible to use embedded Scaffolds in the way I have them at the moment. The problem is that i have a root scaffold called DynamicApp which persists my Drawer and BottomNavigationBar, but loading in other Scaffold pages into the body means the modals are slotting into that body and behind the BottomNavigationBar. To solve the problem you have to subclass BottomNavigationBar and reference it in every Scaffold; which means encapsulating all of the business logic so it uses ChangeNotifier to change state when the nav is interacted with. Basically, Flutter forces a separation of concerns on your architecture, which I guess is a good thing. I'll compose a better answer when I've done all the extra work.
After many hours tearing my hair out trying to pass ScaffoldState keys around, and Navigators the answer was to build the BottomNavigationBar into every Scaffold. But to do this I've had to change how my architecture works ... for the better! I now have a BottomNavigationBar and RootScreenSwitcher that listens for updates from an AppState ChangeNotifier and rebuilds itself when the page index changes. So, I only have to change state in one place for the app to adapt automatically. This is the AppState class:
import 'package:flutter/material.dart';
class AppState extends ChangeNotifier {
int _pageIndex = 0;
int get pageIndex {
return _pageIndex;
}
set setpageIndex(int index) {
_pageIndex = index;
notifyListeners();
}
}
and this is my custom BottomNavigationBar called AppBottomNavigationBar:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBottomNavigationBar extends StatefulWidget {
AppBottomNavigationBar({Key key}) : super(key: key);
#override
_AppBottomNavigationBarState createState() => _AppBottomNavigationBarState();
}
class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: currentIndex ?? 0,
items: bottomNavBarItems,
showUnselectedLabels: false,
onTap: (int index) {
setState(() {
state.setpageIndex = index;
});
},
);
}
}
So, now in my other Scaffold pages I only need to include this line to make sure the BottomNavigationBar is in the Scaffold`:
bottomNavigationBar: AppBottomNavigationBar(),
Which means absolute minimal boilerplate.
I changed the name of the DynamicApp class to AppRootScaffold and this now contains a Scaffold, Drawer, and then set the body of the Scaffold as the RootScreenSwitcher:
class RootScreenSwitcher extends StatelessWidget {
RootScreenSwitcher({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
var state = Provider.of<AppState>(
context,
);
int currentIndex = state.pageIndex;
return SafeArea(
top: false,
child: IndexedStack(
index: currentIndex ?? 0,
children: menuItems.map<Widget>((MenuItem item) {
return item.widget;
}).toList(),
),
);
}
}
I still have a lot to do to streamline this architecture, but it is definitely the better way to go.
UPDATE:
Can you spot the problem with the new Scaffold architecture?
This is still not great. Ironically, I need the BottomNavigationBar back in the root Scaffold for this to work as expected. But then the modals wont appear over the top of the bar again.

Navigation with custom navigator and globalkey

I did what was suggested here for a persistent bottom bar using a custom navigator.
class _MainScreenState extends State<MainScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/page1':
builder = (BuildContext context) => Page1();
break;
case '/page2':
builder = (BuildContext context) => Page2();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
// You can also return a PageRouteBuilder and
// define custom transitions between pages
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
}
Now, if I have child widgets on HomePage with a button that needs to navigate me to a Page2, How would I achieve that from inside HomePage - because?
some button or GestureDetector
onTap : ()=>Navigator.of(context).pushNamed('/page2')
or
onPress : ()=>Navigator.of(context).pushNamed('/page2')

Cannot make bottom navigation bar and routing work together in flutter

I am new to flutter and i cannot navigate to new page from bottom navigation bar
I have main app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
));
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
builder: (BuildContext buildContext, Widget widtget) => Scaffold(
body: RootNavigator(),
bottomNavigationBar: BottomNavigation(),
),
);
}
}
and Rootnavigator
class RootNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
// final args = settings.arguments;
return MaterialPageRoute(
settings: settings,
builder: (BuildContext context) {
switch (settings.name) {
case '/':
return Page1();
case '/page2':
return Page2();
case '/page3':
return Page3();
default:
return RouteErrorPage();
}
});
},
);
}
}
And bottom navigator
class BottomNavigation extends StatefulWidget {
#override
BottomNavigationState createState() {
return new BottomNavigationState();
}
}
class BottomNavigationState extends State<BottomNavigation> {
int currIndex = 0;
onTap(int index) {
setState(() => currIndex = index);
switch (index) {
case 0:
Navigator.pushNamed(context, '/');
break;
case 1:
Navigator.pushNamed(context, '/page2');
break;
case 2:
Navigator.pushNamed(context, 'page3');
break;
default:
Navigator.push(
context, MaterialPageRoute(builder: (_) => RouteErrorPage()));
}
}
....
// added app bar items
}
Tabs are switching but routes are not. It stays on home page.
I feel like there is something with context but do not know how to solve it.
Can anybody help?
Thanks
p.s. if i move bottom navigation bar to each page separatly everything work sexcept selected tab (because of state) and also i want to keep one, shared app bar
Answer is - #LoVe comment was correct.
Thats how flutter works.
if you have bottom navigation you have to swipe through pages.
Redirection means moving to completely new page and there you have to define Scaffold from sratch.
If you want to have shared AppBar - make it reusable widget

Starting a Welcome Screen on first startup

I'm trying to display a few welcome screens on the first launch of my App. After which it will go to the login screen. But for efficiency, I need it to pop all the welcome screens off the 'stack' before launching the login screen, which is the reason behind this request.
My main.dart program loads into preloader2.dart which looks like this:
class PreLoad2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
String myRoute;
return Scaffold(
body: SafeArea(
child: FlatButton(
child: Text('Press me!'),
onPressed: () {
if (loginCount == 0) { // globally defined variable
myRoute = '/welcome1';
} else {
myRoute = '/login';
}
++loginCount;
Navigator.pushNamed(context, myRoute);
},
),
),
);
}
}
It works perfectly well. But I have to click on the button.
I've tried both a stateless widget and a stateful one in preload2.
Can I write some code to replace the button so it just runs with no user input?
Just place the onPressed body into the build function before returning the Scaffold
#override
Widget build(BuildContext context) {
String myRoute;
if (loginCount == 0) // globally defined variable
myRoute = '/welcome1';
else
myRoute = '/login';
++loginCount;
Navigator.pushNamed(context, myRoute);
return Scaffold(
body: SafeArea(
child: FlatButton(
child: Text('Press me!'),
onPressed: () {
// Nothing Here
},
),
),
);
}
}
I have managed to get around, rather than answer my own question.
Inside my main.dart program I have added a new Welcome Widget. What this does is to set up a new Navigator that in turn calls each of my welcome screens. Each one in turn is pushed and then popped. So it is not quite what I was after but it definitely does the trick and appears to be quite efficient. My Welcome widget looks like this:
class Welcome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
initialRoute: 'welcome1',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'welcome1':
builder = (BuildContext _) => Welcome1(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome2':
builder = (BuildContext _) => Welcome2(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome3':
builder = (BuildContext _) => Welcome3(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
case 'welcome4':
// Assume ChooseCredentialsPage collects new credentials and then
// invokes 'onSignupComplete()'.
builder = (BuildContext _) => Welcome4(
onWelcomeComplete: () {
Navigator.of(context).pop();
},
);
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
Each of Welcome2(), Welcome3() and Welcome4() are held in their own files and pass back a link to the next widget, either going forward or backwards through the chain. The button code looks like this:
onPressed: () {
Navigator.of(context).pushReplacementNamed(routeName);
},
Where the route name is 'welcome2', for example, which is then linked to by the switch statement that creates a new route to the required widget.
Once on the last welcome screen (or before if required) the 'finish' button calls the onWelcomeComplete function which simply pops the complete Navigator stack leaving the code to follow the default route ('/') which I set up in the MyApp widget taking me to my Login() widget in login.dart. The code is:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/welcome',
routes: {
'/': (BuildContext context) => Login(),
'/welcome': (BuildContext context) => Welcome(),
...
I hope this answer clearly explains what I have achieved and helps someone else.
Further information about the Flutter Navigator can be found here:
Navigator Class

How to reset the navigator when switching between cupertino tabs

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.