I have view with bottom navigation bar, and when you push a navbar item, a new route is pushed into view.
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) => MaterialApp(
home: Scaffold(
key: _scaffoldKey,
body: _buildBody(context),
bottomNavigationBar: _buildBottomNavigationBar(context),
),
);
void routeToView(routeIndex) {
navigatorKey.currentState.pushNamed(pagesRouteFactories.keys.toList()[routeIndex]);
}
I would like to prevent same route being pushed on the current view. I want to compare the current route with new route we are trying to push, and ignore pushing the route if it is same.
I want to scroll the view to the top if it is same route we are trying to push
Any ideas.
NavigatorState doesn't expose an API for getting the path of the
current route, and Route doesn't expose an API for determining a
route's path either. Routes can be (and often are) anonymous. You can
find out if a given Route is on the top of the navigator stack right
now using the isCurrent method, but this isn't very convenient for
your use case.
https://stackoverflow.com/a/46498543/2554745
This is the closest solution I could think of:
Navigator.of(context).pushNamedAndRemoveUntil(
"newRouteName",
(route) => route.isCurrent && route.settings.name == "newRouteName"
? false
: true);
It will pop the current route if name of the route is "newRouteName" and then push new one, else will not pop anything.
Edit: Latest flutter provides ModalRoute.of(context).settings.name to get the name of current route.
This works for me:
Route route = ModalRoute.of(context);
final routeName = route?.settings?.name;
if (routeName != null && routeName != nav) {
Navigator.of(context).pushNamed(nav);
print(route.settings.name);
}
Related
I have a scenario where I need to use a BottomNavigationBar while ensuring the following:
When changing tabs, where a tab is a particular screen, screens should not refresh, but the state of each screen should be maintained
It must be possible to change tab using both the BottomNavigationBar or Navigator
I am trying to build a proof of concept (on GitHub) considering the above but it is not working as it should be.
In the proof of concept, I have 3 screens:
MainScreen,
Settings
ProfileSettings.
, and only 2 items on the BottomNavigationBar:
MainScreen
Settings
On MainScreen, there is a button that allow navigation to Settings. On Settings, there are two buttons, to Navigate to Settings and ProfileSettings respectively. So ProfileSettings can only be reached from the button on the Settings tab.
If I am on ProfileSettings, I tap on MainScreen on the bottomNavigationBar, once on MainScreen, I tap on Settings, I should see ProfileSettings, this is what I mean by preserving the state.
Below is probably the most important part of the proof of concept. It is part of a page called "PageWithBottomBar.dart" that loads the MainScreen,Settings and ProfileSettings page.
#override
Widget build(BuildContext context) {
....
Scaffold(
appBar: AppBar(title: Text("Trying bottom bar")),
bottomNavigationBar: bottomNavBar,
body: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings){
Widget widget =MainScreen();
if (settings.name == "/main"){
widget = MainScreen();
} else if (settings.name == "/settings"){
widget = Settings();
}
else if (settings.name == "/settings/profile"){
widget = ProfileSettings();
}
return MaterialPageRoute(builder: (context)=> widget,settings: settings);
},
initialRoute: "/main",
),
);
}
Currently, in the proof of concept, I am getting the following problems:
State is not being preserved
When I use Navigator, I can switch to a different tab, but I cannot make a NavigationBarItem active when using Navigator
I use auto_route package for web project to test Navigation 2.0. I use AutoRoureWrapper interface to wrap each page with some common PageScaffold widget, i.e.
class SomePage extends StatefulWidget implements AutoRoureWrapper {
static const title = '/some-page';
...
Widget wrappedRoute(BuildContext context) {
return PageScaffold(
title: title,
body: this,
);
}
...
}
class PageScaffold ... {
}
class _PageScaffoldState ... {
...
#override
Widget build(...) {
return FutureBuilder<User>(
future: futureToGetSidebarItems(),
builder: (context, snapshot) {
if(snapshot.hasData) {
final items = snapshot.data;
final initialRoute = _getInitialRoute(items);
return Row(
children: [
Sidebar(items: items, initialRoute: initialRoute),
const VerticalDivider(width: 1),
Expanded(child: widget.body),
],
);
},
},
);
}
}
I have a Sidebar widget which contains list of clickable links to switch to route, using
AutoRoute.of(context).pushNamed('<route name>');
Now I have a problem when I switch to new page (set new route). Pushing new route forces refresh of all previously (pushed) created instances of PageScaffold and Sidebar widget receives incorrect data. I.e.
Push Dashboard page (after authentication). [PageScaffold(body: Dashboard())] is in history. Network request for user permissions is executed.
Push FirstPage page. [PageScaffold(body: Dashboard()), PageScaffold(body: FirstPage())] is in history. 2 network requests for user permissions (pear each page) are executed.
Push SecondPage page. [PageScaffold(body: Dashboard()), PageScaffold(body: FirstPage()),, PageScaffold(body: SecondPage())] is in history. 3 network requests for user permissions (pear each page) are executed.
And so on...
Why so? Why all pushed pages are refreshed and not only last one? Or what I do wrong?
P.S. When I use default Navigation 1.0 it looks good. Yes, all pushed pages accumulates in history but only latest on is refreshed.
This structure of my app navigation:
CupertinoTabScaffold ->tabBuilder:TabPage-> tabBar: CupertinoTabBar
My TabPage:
return MaterialApp(
navigatorKey: navKey,
home: child,
);
I have separate navigation in each tab. by clicking on the bottom tab, I go back to the beginning. this is my code and this is working now:
key.currentState!.pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
for each tab I have my own navigation key. I pass it to each TabPage.
but if the page is at the root I need to avoid pushing same route twice. I tried this code but it doesn't work:
key.currentState!.pushNamedAndRemoveUntil("/",
(route) => route.isFirst && route.settings.name == "/" ? false : true);
how avoid pushing same route twice?
Navigator.of(context).pushNamedAndRemoveUntil(
"newRouteName",
(route) => route.isCurrent && route.settings.name == "newRouteName"
? false
: true);
t will pop the current route if name of the route is "newRouteName" and then push new one, else will not pop anything.
Edit: Latest flutter provides ModalRoute.of(context).settings.name to get the name of current route.
If you're using a BottomNavigationBar, you can simply add a checker on onTap() to see if you're going to navigate on the same page. To track the current page, you can either use enum or a simple variables like int. CupertinoTabBar also have an onTap property available similar to BottomNavigationBar.
BottomNavigationBar(
items: <BottomNavigationBarItem>[...],
currentIndex: _currentPage,
onTap: (value){
// Only navigate if a different page was clicked
if(value != _currentPage){
_currentPage = value;
setState(() {
switch(value) {
// Page 0
case 0:
key.currentState!.pushReplacementNamed(...);
break;
// Page 1
case 1:
key.currentState!.pushReplacementNamed(...);
break;
...
}
});
}
}
)
Or if you're using a TabBar with TabBarView, the page displayed can be managed by the controller as demonstrated in this guide.
This will pop the current route if name of the route is the new Route Name and then push new one, else will not pop any route.
Navigator.of(context).pushNamedAndRemoveUntil(
"new",
(route) => route.isCurrent && ModalRoute.of(context).settings.name == "new"
? false
: true);
EDIT: Originally this post was specific to using a global navigator but I have found that the case was not specific to using a global navigator. Rather, its a bug that can happen from misuse of the Navigator API.
Flutter 1.22.0 • channel beta • https://github.com/flutter/flutter.git
Framework • revision d408d302e2 (4 weeks ago) • 2020-09-29 11:49:17 -0700
Engine • revision 5babba6c4d
Tools • Dart 2.10.0
I have followed the recommendation in a previous SO post. This works to some extent:
How to navigate without context in flutter app?
But, I have come across some issues with simply using a global navigator key. I am not able to call popUntil with a route predicate to navigate back to my home screen, and because I cannot view the Navigator history stack, I am not able to effectively debug this.
/// constants/keys.dart
final GlobalKey<NavigatorState> navKey = new GlobalKey<NavigatorState>();
/// constants/routes.dart
const String HOME = 'home';
const String SCREEN_A = 'a';
const String SCREEN_B = 'b';
/// main.dart
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
navigatorKey: Keys.navKey,
onGenerateRoute: (routeSettings) {
switch (routeSettings.name) {
case Routes.HOME:
return MaterialPageRoute(
builder: (context) => Home(),
settings: RouteSettings(name: Routes.HOME),
);
case Routes.SCREEN_A:
return MaterialPageRoute(
builder: (context) => ScreenA(),
settings: RouteSettings(name: Routes.SCREEN_A),
);
case Routes.SCREEN_B:
return MaterialPageRoute(
builder: (context) => ScreenB(),
settings: RouteSettings(name: Routes.SCREEN_B),
);
default:
return MaterialPageRoute(
builder: (context) => Home(),
settings: RouteSettings(name: Routes.HOME),
);
}
},
);
}
}
This is a tangent to show that you cannot use pages to view history added using the Navigator API.
/// Experiment, instrument build of Home, ScreenA, ScreenB with code to view pages:
Navigator nav = Keys.navKey.currentWidget;
print("Location: Home, Number of pages: ${nav.pages.length}");
for (Page page in nav.pages) {
print("Page: ${page.name}, ${page.key}, ${page.toString()}");
}
/// OUTPUT:
// Home, Number of pages: 0
// ScreenA, Number of pages: 0
// ScreenB, Number of pages: 0
I did not expect pages to display the navigation history
Some Routes do not correspond to Page objects, namely, those that are
added to the history using the Navigator API (push and friends). A
Route that does not correspond to a Page object is called a pageless
route and is tied to the Route that does correspond to a Page object
that is below it in the history.
End Tangent
The main pain point I have with Navigator is my need to navigate from within methods that do not have a build context in scope and require a global key to access widgets such as a Navigator. For example, I have to attach a listener for failure of a blue tooth connection and then navigate back to the HOME screen from the listener. In order to do so, I have had to rely on the global nav key. I also prefer having access to a global nav key instead of calling Navigator.of(context).
However, because I cannot view the Navigator history stack, I cannot easily debug my issue.
I am able to navigate forward using these calls with the global nav key:
Keys.navKey.currentState.pushNamed(Routes.HOME);
And I can use either:
Keys.navKey.currentState.pushNamedAndRemoveUntil(Routes.HOME, (Route<dynamic> route) => false);
or:
Keys.navKey.currentState.pushReplacementNamed(Routes.HOME);
to navigate backward.
But I can't simply pop off the internal Navigator history stack without getting a black screen.
/// Black Screen
Keys.navKey.currentState.popUntil(ModalRoute.withName(Routes.HOME));
To me, it seems like the correct method would be popUntil as it pops everything off the stack except the required route. Why is it not possible to simply call popUntil to navigate back to the home screen safely in this example?
I am able to navigate back to the default Route using this snippet:
Keys.navKey.currentState.popUntil(ModalRoute.withName(Navigator.defaultRouteName));
Using the debugger, I see that the default route name is '/' and not 'home'. Using the navigator to programmatically navigate does not push the initial route onto the stack as home. This makes sense. I never call Keys.navKey.currentState.push('home'). This is something to keep in mind if you want to return to your app default screen using the navigator, the above method call seems to be the correct way to do it, unless you find a way to set the bottom of your Navigator stack to your home route.
Consider I have 3 screens namely screen 1, screen 2, screen 3 and screen 4.
I want to achieve the following.
Screen 3 is opened by Screen 1 -> BackButton -> Screen 2
Screen 3 is opened by Screen 2 -> BackButton -> Screen 3
Screen 3 is opened by Screen 4 -> BackButton -> Screen 1
Moreover, iOS automatically sets a swipe back option. I want to overwrite it that a swipe back in iOS does the same as described above.
Is there something like conditional routing in Flutter which helps me to adjust the BackButton-behaviour in accordance to 'from which Screen was my current Screen opened (navigator.push)'?
Wrap your widget tree in a WillPopScope() widget. This widget has an onWillPop property that you can override to whatever you want - in this case, depending on the screen you're on you'll probably want to override it to
onWillPop: () => Navigator.pushReplacement(<correctScreenWidget>)
This should catch any attempts to go back and instead do whatever you override it to. Be sparing with it, overriding default back button behaviour can make for a weird user experience if done poorly.
As for the conditional part of it, unfortunately it's a bit tricky as Navigator._history is private, so we can't just check the previous route that way. Best bet is to set up a NavigatorObserver to keep track of previous routes, and set the name in the RouteSettings of each of your routes to keep track.
Step one is to create an observer and provide it to your Navigator, something like this:
class PreviousRouteObserver extends NavigatorObserver {
Route _previousRoute;
Route get previousRoute => _previousRoute;
String get previousRouteName => _previousRoute.settings.name;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_previousRoute = previousRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_previousRoute = oldRoute;
}
}
class MyApp extends StatelessWidget {
final PreviousRouteObserver observer = PreviousRouteObserver();
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page', observer: observer),
navigatorObservers: [
observer,
],
);
}
}
Note that MyHomePage above needs to accept the observer as an argument so you can access it. Alternatively you could set up an InheritedWidget or something to maintain access to it, but this answer is getting a little long already so I'll leave that for a later question.
Then, when providing Routes to your Navigator, ensure you've got a name in the RouteSettings:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => NextScreen(observer: widget.observer),
settings: RouteSettings(name: "nextScreen"),
),
);
Finally, do conditional routing based on the current value of widget.observer.previousRouteName in any widget that has access to it. This is just a simple matter of a switch or something in your onWillPop, so I'll leave that to you.
Kind of unfortunate that it's so roundabout, but it looks like this might be your best option at the moment. Hope it helps!