Flutter Toggle overlay widget from every screen - flutter

I have a problem. In my main.dart I have a IndexedStack with a bottomNavigationBar. The IndexedStack exists of 4 screens, and one of the screens contains a Navigator inside that screen. Now in the bottomNavigationBar, I have a custom bar like this:
I want the Navigator to switch screens when I click on that bar, but because that custom bottom bar is not part of the Navigator, I can't call:
Navigator.pushNamed(context, 'location_details');
Because that results in the following error:
FlutterError (Could not find a generator for route RouteSettings("location_details", null) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
1. For the "/" route, the "home" property, if non-null, is used.
2. Otherwise, the "routes" table is used, if it has an entry for the route.
3. Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
4. Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.)
Here is the main.dart code:
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int currentIndex = 0;
#override
Widget build(BuildContext context) {
final screens = [
LocationStateManager(),
MySessionsPage(),
MapPage(),
SettingsPage()
];
return Scaffold(
appBar: AppBar(
title: Text(widget.title,
style: TextStyle(
color: PRIMARY,
fontSize: 32,
fontFamily: 'JotiOne',
fontWeight: FontWeight.bold)),
centerTitle: true,
backgroundColor: DARK_BACKGROUND_PRIMARY,
),
body: SafeArea(
child: IndexedStack(index: currentIndex, children: screens),
),
bottomNavigationBar: new Theme(
data:
Theme.of(context).copyWith(canvasColor: DARK_BACKGROUND_PRIMARY),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
(isVisible)
? BottomBarTraveling()
: SizedBox(
height: 0,
),
BottomNavigationBar(
type: BottomNavigationBarType.fixed,
iconSize: 30,
currentIndex: currentIndex,
onTap: (index) => setState(() => currentIndex = index),
showSelectedLabels: false,
showUnselectedLabels: false,
selectedItemColor: NAVBAR_SELECTED,
unselectedItemColor: NAVBAR_UNSELECTED,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: ''),
BottomNavigationBarItem(icon: Icon(Icons.list), label: ''),
BottomNavigationBarItem(icon: Icon(Icons.map), label: ''),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: '')
],
)
],
),
));
}
}
And here is the LocationStateManager code which contains the Navigator:
class LocationStateManager extends StatefulWidget {
#override
_LocationStateManagerState createState() => _LocationStateManagerState();
}
class _LocationStateManagerState extends State<LocationStateManager> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: null,
body: Stack(children: <Widget>[
Navigator(
onGenerateRoute: (settings) {
switch (settings.name) {
case 'location_list':
return MaterialPageRoute(
builder: (context) => LocationListPage(),
);
case 'location_details':
return MaterialPageRoute(
builder: (context) =>
KiteupLocationPage(),
);
case 'kiteup_status_page':
return MaterialPageRoute(
builder: (context) => KiteupStatusPage(),
);
default:
return MaterialPageRoute(
builder: (context) => LocationListPage(),
);
}
},
),
]));
}
}
How can I change the Navigator route to location_details, by clicking on the BottomBarTraveling() bar, that is placed a level above the Navigator.
PS: Inside the BottomBarTraveling() is the gesturedetector which has the
onTap: () { Navigator.pushNamed(context, 'location_details'); }

Navigator is default navigator of flutter, in this case you want to deal with custom navigator (or neste navigator), you must decrate your ow navigator and use it insteads of Navigator. The point is create navigator = GlobalKey<NavigatorState>() and passing to key props of Navigator.
Update: Of course any function of navigator like pop/push/pushName on your custom navigator need using navigator instead of Navigator.
Example: A simple app with button out of Navigator and 2 screen with differences color inside Navigator, push button and Navigator was changed.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: MyHomePage()));
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static final navigator = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
Expanded(
child: Navigator(
key: navigator,
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const Sc1());
case 'sc2':
default:
return MaterialPageRoute(builder: (_) => const Sc2());
}
},
),
),
ElevatedButton(
child: const Text('To Sc2'),
onPressed: () {
navigator.currentState?.pushNamed('sc2');
},
),
],
),
);
}
}
// screen 1 with green color
class Sc1 extends StatelessWidget {
const Sc1({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: const Center(child: Text('Screen 1')),
);
}
}
// screen 2 with blue color
class Sc2 extends StatelessWidget {
const Sc2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: const Center(child: Text('Screen 2')),
);
}
}
Addtional: In case you want to calling navigator outside of MyHomePage, you can declare an static like this and calling it from child nodes
class MyHomePage extends StatefulWidget {
static MyHomePageState of(BuildContext context) =>
context.findAncestorStateOfType<MyHomePageState>()!;
...
}
call() {
MyHomePage.of(context).navigator.push(...);
}

Related

Why doesn't BottomNavigatorBarItem currentIndex get updated?

I am new to flutter and I did find similar questions on SO but am too novice to understand the nuance so apologies if this question is too similar to an already asked one.
I have a BottomNavigationBar which has 3 icons (Home, Play and Create) which should navigate between these 3 routes/pages.
main.dart
routes: {
"/home": (context) => MyHomePage(title: "STFU"),
"/play": (context) => Play(),
"/create": (context) => Create(),
"/settings": (context) => Settings(),
},
I extracted my navbar into a custom class so my 3 separate pages could use it:
bottom-nav.dart
class MyBottomNavBar extends StatefulWidget {
MyBottomNavBar({
Key? key,
}) : super(key: key);
#override
State<MyBottomNavBar> createState() => _MyBottomNavBarState();
}
class _MyBottomNavBarState extends State<MyBottomNavBar> {
int _selectedIndex = 0;
void _onTapped(int index) => {
print("_onTapped called with index = $index"),
setState(
() => _selectedIndex = index,
)
};
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: Colors.orangeAccent[100],
currentIndex: _selectedIndex,
onTap: (value) => {
print("value is $value"),
// find index and push that
_onTapped(value),
if (value == 0)
{Navigator.pushNamed(context, "/home")}
else if (value == 1)
{Navigator.pushNamed(context, "/play")}
else if (value == 2)
{Navigator.pushNamed(context, "/create")}
},
items: [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home_filled)),
BottomNavigationBarItem(
label: "Play", icon: Icon(Icons.play_arrow_rounded)),
BottomNavigationBarItem(label: "Create", icon: Icon(Icons.create)),
],
);
}
}
so now i just set this MyBottomNavBar class to the bottomNavigationBar property of the Scaffold widget my inside Home page, Play page and Create page for eg.
home.dart
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home"),
),
body: Column(
children: [
Container(
padding: EdgeInsets.all(20.00),
child: Text("inside home"),
),
],
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
play.dart
class Play extends StatefulWidget {
const Play({super.key});
#override
State<Play> createState() => _PlayState();
}
class _PlayState extends State<Play> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("inside play page"),
SizedBox(
height: 30.00,
),
Text("some text"),
],
),
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
The nav bar buttons work to switch between pages but for some reason the currentIndex value isn't getting updated and stays at 0 (i.e on the "Home" icon). When I debug it I can see _selectedIndex getting updated inside inside the _onTapped function which should update the currentIndex value but it doesn't appear to do so. Any help would be appreciated
What you have here is three different pages with their separate BottomNavBar class instance. Instead you should have a shared Scaffold and one bottomNavBar so that when you navigate bottomNavbar state does not reset.
You can use PageView to do this.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
}
List<Widget> pages = const [Home(), Play(), Create()];
final _pageController = PageController();
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
_pageController.jumpToPage(index);
_selectedIndex = index;
setState(() {});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.play_arrow), label: 'Play'),
BottomNavigationBarItem(icon: Icon(Icons.create), label: 'Create'),
],
backgroundColor: Colors.greenAccent,
),
body: PageView(
controller: _pageController,
children: pages,
),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Play extends StatelessWidget {
const Play({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Create extends StatelessWidget {
const Create({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}

My screen doesn't reflect the changes in my app though the setState method works well

I'm trying to call a StatefulWidget(i.e FirstPage()) within a MaterialApp. I'm pretty much new to flutter and I don't know where I went wrong. According to my knownledge I've used StatefulWidget to tell flutter my screen on that page is going to encounter some changes in UI. But I got no idea to fix this error.
main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter_project/main.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage());
}
}
class FirstPage extends StatefulWidget {
const FirstPage({Key? key}) : super(key: key);
#override
State<FirstPage> createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
String buttonName = "Click";
int currentIndex = 0;
return Scaffold(
appBar: AppBar(
title: const Text("App title "),
),
body: Center(
child: currentIndex == 0
? Container(
width: double.infinity,
height: double.infinity,
color: Colors.blueAccent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 280,
height: 80,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.circular(18),
),
backgroundColor: const Color.fromRGBO(9, 8, 99, 90),
foregroundColor: Colors.red,
),
onPressed: () {
setState(() {
buttonName = "Clicked";
//print(buttonName0);
});
},
child: Text(buttonName),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
//'BuildContext' - datatype and 'context' - variable name
return const SecondPage();
},
),
);
},
child: const Text("Move to new page"),
),
],
),
)
: Image.asset("images/img.png"),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home)),
BottomNavigationBarItem(label: "Settings", icon: Icon(Icons.settings))
],
currentIndex: currentIndex,
onTap: (int index) {
//index value here is returned by flutter by the function 'onTap'
setState(() {
currentIndex = index;
//print(currentIndex);
});
},
),
); //Scaffold represents the skeleton of the app(displays white page)
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
Images:
Before pressing Click and Settings button
After pressing Click and Settings looks the same
I want the screen to change the ElevatedButton Click to Clicked when onPressed() is triggered and also, the app should be able to switch settings page when the onTap() method is triggered in the bottom navigation bar.
The code worked initially when I refrained from calling an entire page of Scaffold from Material app, but as soon as I changed the part
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage()); //<-- this part
}
}
I'm getting this error.
Put your variables outside the build method.Else it will reset to default on every build.
It will be like
class _FirstPageState extends State<FirstPage> {
//here
String buttonName = "Click";
int currentIndex = 0;
#override
Widget build(BuildContext context) {
// Not here
return Scaffold(
appBar: AppBar(
More about StatefulWidget

Correctly managing Flutter pages with bottom navigation, and using state to trigger rebuild with setState()

My app is now being broken down into multiple pages (separate dart files) with a bottom Navigation bar. The first page is a map, and every time I go to a different page, I return and the map widget rebuilds itself and re-initialises (markers, map.center and map.zoom, etc).
So, I introduced an IndexedStack() which has the desired effect of keeping the pages in their original state. However, when I make some changes I'd like to trigger a rebuild with setState((){}), however the error I'm then getting is
[ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: setState() called in constructor: _PageMapState#52149(lifecycle state: created, no widget, not mounted)
So this leads me to think that the way I'm creating and displaying these pages is perhaps wrong, as I'm trying to setState() on something that doesn't yet exist (or a different version of it)
I'd like to see if I'm using the correct methods to manage separate pages in the app state and navigation. My current setup with the IndexedStack is like so:
Main App
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'My app'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget _widgetOptionsPages(int index) {
switch (index) {
case 0:
return PageMap();
break;
case 1:
return PageSearch();
break;
case 2:
return PageDownloads();
break;
case 3:
return PageSettings();
break;
case 4:
return PageAbout();
break;
}
return PageMap();
}
#override
Widget build(BuildContext context) {
return
Scaffold(
// appBar: AppBar(
// title: const Text('Title'),
// ),
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => btn1(),
child: const Text('BTN 1'),
),
ElevatedButton(
onPressed: () => btn2(),
child: const Text('BTN 2'),
),
ElevatedButton(
onPressed: () => btn3(),
child: const Text('BTN 3'),
),
],
),
),
bottomNavigationBar: SalomonBottomBar(
duration: Duration(milliseconds: 300),
itemPadding: EdgeInsets.all(15),
margin: EdgeInsets.fromLTRB(20,10,20,10),
currentIndex: _currentIndex,
onTap: (i) => setState(() => _currentIndex = i),
items: [
/// Nav Item to Map
SalomonBottomBarItem(
icon: Icon(Icons.map_sharp),
title: Text("Map"),
selectedColor: Colors.purple,
),
/// Nav Item to About
SalomonBottomBarItem(
icon: Icon(Icons.help),
title: Text("About"),
selectedColor: Colors.deepOrangeAccent,
),
],
),
body: _widgetOptionsPages(_currentIndex),
);
}
About Page (Example About page fragment. Map is the first page, and similar)
class PageAbout extends StatelessWidget {
const PageAbout({Key? key}) : super(key: key);
#override
Widget build (BuildContext context){
return Scaffold(
appBar: AppBar(
title: const Text('About'),
), body: Center(child: Text("About")));
}
}
I started looking at the following methods in MainApp, but they kept forcing the map to rebuild and reposition back to the starting pos (mentioned earlier)
static final List<Widget> _pagesFragments = [
PageMap(),
...
PageAbout(),
];
and
body: IndexedStack(
index: _currentIndex,
children: _pagesFragments,
),
),
Many thanks

How to show navigation bar on page which doesn't belong to this bar?

edit: SOLUTION
i used package custom_navigator
In navigation bar I have 2 pages to redirect, but I want to navigate to third page and still want to see navigation bar (this one with 2 pages) there.
Is it possible to do? Do I have to make my own navigation bar for this page?
class Bar extends StatefulWidget {
#override
BarState createState() => BarState();
}
class BarState extends State<Bar> {
int tabIndex = 0;
List<Widget> pages = [
FirstPage(),
SecondPage(),
];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: SafeArea(
child: BottomNavigationBar(
iconSize: 25,
elevation: 4.0,
items: <BottomNavigationBarItem>[
barItem(Icons.message),
barItem(Icons.camera_enhance),
barItem(Icons.person),
],
currentIndex: tabIndex,
onTap: (int index) {
setState(() {
tabIndex = index;
});
},
),
)),
body: Container(
child: pages.elementAt(tabIndex),
),
);
}
}
this is what i try:
List<Widget> pages = [
Container(
color: Colors.green,
child: Center(
child: ElevatedButton(
onPressed: state
),
),
),
SecondPage(),
ThirdPage()
];
state() {
tabIndex = 2;
setState(() {
});
}
The simplest way, if you don't mind it animating would be to init an AppBar in your navigator and pass it to the pages and they would use it in there scaffold.
class MyFlow extends StatefulWidget {
const MyFlow({Key? key}) : super(key: key);
#override
_MyFlowState createState() => _MyFlowState();
}
class _MyFlowState extends State<MyFlow> {
#override
Widget build(BuildContext context) {
final appBar = AppBar();
return Navigator(
onPopPage: (route, result) => true,
pages: [
MaterialPage(child: PageOne(appBar: appBar)),
MaterialPage(child: PageTwo(appBar: appBar)),
MaterialPage(child: PageThree(appBar: appBar)),
],
);
}
}
class PageOne extends StatelessWidget {
const PageOne({Key? key, required this.appBar}) : super(key: key);
final AppBar appBar;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
);
}
}

Flutter Navigator 2.0: Named routes with Drawer Navigation

I'm working on a simple navigation on an app that follows Flutter Navigator 2.0 to have better support on routes. An advantage of Navigator 2.0 is that you have granular controls on screen navigations and have better support for web.
Here's a diagram of what I'm trying to achieve.
I'm able to navigate from the LoginScreen to the "main page" without issues. The main page has a Drawer that displays HomeScreen by default. My issue here is that I'm unsure on how to properly display HomeScreen and ProfileScreen from the Drawer using Navigator 2.0. Following this guide demonstrates that a screen is pushed to the Navigator stack and routes are tracked with a RouterDelegate. Doing so pushes a new screen to the stack.
The app seems to work fine, but I just removed the transition animation. You can notice that a new screen is drawn even before the Drawer can finish its closing animation. The entire Drawer widget is drawn again since the main page is pushed to the stack.
This guide on displaying screens on a Drawer only replaces the widget on the same screen. What I'm currently doing is I'm rebuilding the main page every time it navigates to HomeScreen and ProfileScreen
Here's how the main page looks like. The currentPage is updated to display the Widget for the HomeScreen and ProfileScreen.
late Widget currentPage;
#override
Widget build(BuildContext context) {
currentPage = HomeScreen();
return Scaffold(
appBar: AppBar(title: Text(title)),
body: currentPage,
drawer: Drawer(
child: ListView(
children: <Widget>[
// ...
ListTile(
title: Text('Home'),
onTap: () {
widget.navHome();
Navigator.pop(context);
},
),
ListTile(
title: Text('Profile'),
// ...
),
],
),
),
);
}
The Navigator on my RouterDelegate has this setup.
Navigator(
key: navigatorKey,
transitionDelegate: NoAnimationTransitionDelegate(),
pages: [
if (show404)
MaterialPage(
key: ValueKey('UnknownPage'),
child: UnknownScreen(),
)
else if (page == Pages.home)
MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(
title: 'Home',
handleLogout: _logOut,
navHome: _navHome,
navProfile: _navProfile,
currentScreen: HomeScreen(username: username),
),
)
else if (page == Pages.profile)
MaterialPage(
key: ValueKey('ProfilePage'),
child: HomePage(
title: 'Profile',
handleLogout: _logOut,
navHome: _navHome,
navProfile: _navProfile,
currentScreen: ProfileScreen(),
),
)
else // username is null, no user logged in
MaterialPage(
key: ValueKey('LoginPage'),
child: LoginPage(
title: 'Login',
onTapped: _handleLogin,
),
),
]
)
The functions are passed as arguments to update the routes in the RouterDelegate.
enum Pages { login, home, profile }
class PageRouterDelegate extends RouterDelegate<PageRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<PageRoutePath> {
// ... Other lines omitted for simplicity
void _handleLogin(String username, String password) {
// TODO Auth
this.page = Pages.home;
notifyListeners();
}
void _navHome() {
this.page = Pages.home;
notifyListeners();
}
void _navProfile() {
this.page = Pages.profile;
notifyListeners();
}
void _logOut() {
this.page = Pages.login;
notifyListeners();
}
}
Any suggestions on what approach can be done to display different screens from a Drawer and have their routes tracked using Navigator 2.0?
Update:
I found a solution for this issue, but it'll need to use two Navigators. Assigning keys to the Navigators helps us manage both of them.
The second Navigator is on the Home page that has a Drawer. This enables us to navigate through different pages without rebuilding the entire screen. The caveat in this approach is that only the routes on mainNavigator are displayed on the address bar on web.
Let me know if you have other suggestions on how this can be approached.
I gave up on using Navigator 2.0 for now due to the amount of boiler plate code that I need to write. I took a step back and used Navigator 1.0 with named routes. The same principle can still be applied to Navigator 2.0
What I did now is instead of building a different Screen on the main Navigator, I still rebuild the same screen(NavigatorPage) to save resources and pass arguments for the screen that I'd like to navigate to.
final _mainNavigatorKey = GlobalKey<NavigatorState>();
...
MaterialApp(
title: 'Navigator Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorKey: _mainNavigatorKey,
routes: {
/// [title] updates the title on the main AppBar
/// [route] NavigatorPage Router depends on route defined on this parameter
/// [showDrawer] show/hide main AppBar drawer
Nemo.home: (context) => NavigatorPage(
title: 'Home',
route: Nemo.home,
navigatorKey: _mainNavigatorKey,
showDrawer: true,
),
Nemo.post: (context) => NavigatorPage(
title: 'Post',
route: Nemo.post,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
Nemo.profile: (context) => NavigatorPage(
title: 'Profile',
route: Nemo.profile,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
Nemo.settings: (context) => NavigatorPage(
title: 'Settings',
route: Nemo.settings,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
},
);
The NavigatorPage contains the main AppBar where we can update the title and display/hide a LinearProgressBar. From there, we can navigate to the desired Screen by checking the route passed in the arguments with the _mainNavigatorKey as our Navigator.
Navigator(
// key: _navigatorKey,
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
// switch (settings.name) {
switch (widget.route) {
/// Default page displayed on Home Screen
case Nemo.home:
builder = (BuildContext context) => _homePage();
break;
case Nemo.post:
builder = (BuildContext context) => _postPage();
break;
case Nemo.profile:
builder = (BuildContext context) => _profilePage();
break;
case Nemo.settings:
builder = (BuildContext context) => _settingsPage();
break;
default:
builder = (BuildContext context) => const UnknownPage();
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
While we're still rebuilding a page on the main Navigator. A workaround for the janky animation when the Navigation Drawer is closed is to add a delay of at least 300ms to wait for the animation to finish before executing the navigation. You can adjust the delay as you see fit.
ListTile(
title: const Text('Home'),
onTap: () {
// Close the drawer
Navigator.pop(context);
/// [drawerDelay] gives time to animate the closing of the Drawer
Timer(Duration(milliseconds: drawerDelay), () async {
widget.navigatorKey.currentState!.pushNamed(Nemo.home);
});
},
),
Demo
Sample Code
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final _mainNavigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigator Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// home: const NavigatorPage(title: 'Flutter Demo Home Page'),
navigatorKey: _mainNavigatorKey,
routes: {
/// [title] updates the title on the main AppBar
/// [route] NavigatorPage Router depends on route defined on this parameter
/// [showDrawer] show/hide main AppBar drawer
Nemo.home: (context) => NavigatorPage(
title: 'Home',
route: Nemo.home,
navigatorKey: _mainNavigatorKey,
showDrawer: true,
),
Nemo.post: (context) => NavigatorPage(
title: 'Post',
route: Nemo.post,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
Nemo.profile: (context) => NavigatorPage(
title: 'Profile',
route: Nemo.profile,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
Nemo.settings: (context) => NavigatorPage(
title: 'Settings',
route: Nemo.settings,
navigatorKey: _mainNavigatorKey,
showDrawer: true),
},
);
}
}
class NavigatorPage extends StatefulWidget {
const NavigatorPage(
{Key? key,
required this.title,
required this.route,
required this.navigatorKey,
required this.showDrawer})
: super(key: key);
final String title;
final String route;
final bool showDrawer;
final GlobalKey<NavigatorState> navigatorKey;
#override
State<NavigatorPage> createState() => _NavigatorPageState();
}
class _NavigatorPageState extends State<NavigatorPage> {
// final _navigatorKey = GlobalKey<NavigatorState>();
/// Drawer delay let's us have the Navigation Drawer close first
/// before the navigating to the next Screen
int drawerDelay = 300;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: widget.showDrawer
? Drawer(
/// TODO return null to hide Drawer if in Login/Registration page
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.blue,
),
child: Text('Drawer Header'),
),
ListTile(
title: const Text('Home'),
onTap: () {
// Close the drawer
Navigator.pop(context);
/// [drawerDelay] gives time to animate the closing of the Drawer
Timer(Duration(milliseconds: drawerDelay), () async {
widget.navigatorKey.currentState!.pushNamed(Nemo.home);
});
},
),
ListTile(
title: const Text('Profile'),
onTap: () {
// Close the drawer
Navigator.pop(context);
Timer(Duration(milliseconds: drawerDelay), () async {
widget.navigatorKey.currentState!
.pushNamed(Nemo.profile);
});
},
),
ListTile(
title: const Text('Settings'),
onTap: () {
// Close the drawer
Navigator.pop(context);
Timer(Duration(milliseconds: drawerDelay), () async {
widget.navigatorKey.currentState!
.pushNamed(Nemo.settings);
});
},
),
],
),
)
: null,
body: Navigator(
// key: _navigatorKey,
/// initialRoute needs to be set to '/'
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
// switch (settings.name) {
switch (widget.route) {
/// Default page displayed on Home Screen
case Nemo.home:
builder = (BuildContext context) => _homePage();
break;
case Nemo.post:
builder = (BuildContext context) => _postPage();
break;
case Nemo.profile:
builder = (BuildContext context) => _profilePage();
break;
case Nemo.settings:
builder = (BuildContext context) => _settingsPage();
break;
default:
builder = (BuildContext context) => const UnknownPage();
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
);
}
Widget _homePage() =>
HomePage(title: 'Home', navigatorKey: widget.navigatorKey);
Widget _postPage() =>
PostPage(title: 'Post', navigatorKey: widget.navigatorKey);
Widget _profilePage() =>
ProfilePage(title: 'Profile', navigatorKey: widget.navigatorKey);
Widget _settingsPage() =>
SettingsPage(title: 'Settings', navigatorKey: widget.navigatorKey);
}
class Nemo {
static const home = '/';
static const login = '/login';
static const register = '/register';
static const post = '/post';
static const profile = '/profile';
static const settings = '/settings';
}
/// Constant values for UI elements
class Constants {
static const String webVersion = 'web-0.1.9-dev';
static const double paddingSmall = 8.0;
static const double paddingNormal = 16.0;
static const double heightNormal = 64.0;
static const double heightThreadCard = 72.0;
static const double heightButtonNormal = 42.0;
static const double widthButtonNormal = 160.0;
}
class UnknownPage extends StatefulWidget {
const UnknownPage({Key? key}) : super(key: key);
#override
State<UnknownPage> createState() => _UnknownPageState();
}
class HomePage extends StatefulWidget {
const HomePage({Key? key, required this.title, required this.navigatorKey})
: super(key: key);
final String title;
final GlobalKey<NavigatorState> navigatorKey;
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Home',
),
ElevatedButton(
child: const Text('View Post Page'),
onPressed: () {
widget.navigatorKey.currentState!.pushNamed(Nemo.post);
},
),
],
),
),
);
}
}
class PostPage extends StatefulWidget {
const PostPage(
{Key? key, required this.title, this.id, required this.navigatorKey})
: super(key: key);
final String title;
final String? id;
final GlobalKey<NavigatorState> navigatorKey;
#override
State<PostPage> createState() => _PostPageState();
}
class _PostPageState extends State<PostPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Id from Route: ${widget.id}',
),
],
),
),
);
}
}
class SettingsPage extends StatefulWidget {
const SettingsPage(
{Key? key, required this.title, required this.navigatorKey})
: super(key: key);
final String title;
final GlobalKey<NavigatorState> navigatorKey;
#override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Settings',
),
ElevatedButton(
child: const Text('View Details'),
onPressed: () {
widget.navigatorKey.currentState!.pushNamed(Nemo.post);
},
),
],
),
),
);
}
}
class ProfilePage extends StatefulWidget {
const ProfilePage({Key? key, required this.title, required this.navigatorKey})
: super(key: key);
final String title;
final GlobalKey<NavigatorState> navigatorKey;
#override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Profile',
),
ElevatedButton(
child: const Text('View Details'),
onPressed: () {
widget.navigatorKey.currentState!.pushNamed(Nemo.post);
},
),
],
),
),
);
}
}
class _UnknownPageState extends State<UnknownPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text(
'404',
),
],
),
),
);
}
}
If you'd like to have a sample with basic Login and Register Screen included. I've created this template that you can check: https://github.com/omatt/flutter-navdrawer-template
It's not a direct answer, just maybe a reference of how to solve this problem.
Flutter recently updated routing example and in that example, they use InheritedWidget with ChangeNotifier to push new route to route delegate like RouteStateScope.of(context)!.go('/book/${book.id}'); while keeping state of routing alive.