I am making an app that has a persistent BottomNavigationBar. Depending on user != null the user gets into the LoginScreen or HomeScreen
There are two ways to get this done by routing:
First approach: include LoginScreen inside the nested Navigator from the MainScaffold
the mainScaffold the user gets redirected from, will have a body that has its own Navigator class inside that body.
App
class AppName extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MainScaffold(),
);
}
}
RouteGenerator
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
dynamic builder;
switch (settings.name) {
case '/':
builder = LoginScreen();
break;
case '/mainScaffold':
builder = MainScaffoldBody();
break;
case '/mainScaffold/page2':
builder = (BuildContext context) => Page2();
break;
...
default:
return _errorRoute();
}
return PageRouteBuilder(
...
pageBuilder: (BuildContext context, _animation, __animation) => builder,
...
);
}
MainScaffold
class MainScaffold extends StatelessWidget {
...
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBarSelector(),
body: Navigator(
key: _navigatorKey,
initialRoute: '/mainScaffold/home',
onGenerateRoute: RouteGenerator.generateRoute
},
),
bottomNavigationBar: CustomBottomNavigationBar(controller: _controller),
);
}
}
Alternative: don't include LoginScreen inside the nested Navigator from the MainScaffold but seperate it
The alternative approach would be to have MainScaffold as initialRoute and LoginScreen inside the bodyof the MainScaffold.
If I want the BottomNavigation to dissapear on for example LoginScreen, I could simply toggle the sized/visibility of that bar. The RouteGenerator would look like such.
App
class AppName extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute, // this static creates possibility of using parameters
);
}
}
RouteGenerator
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
dynamic builder;
switch (settings.name) {
case '/':
builder = MainScaffold();
break;
case '/login':
builder = LoginScreen();
break;
case '/mainScaffold/home':
builder = HomeScreenArguments();
break;
...
default:
return _errorRoute();
}
return PageRouteBuilder(
...
pageBuilder: (BuildContext context, _animation, __animation) => builder,
...
);
}
So what is the best approach here:
LoginScreen is part of the nested Navigator from MainScaffold
LoginScreen is not part of the nested Navigator from MainScaffold
Related
One case I've looked up where this is used is Navigator with WillPopScope as the parent as mentioned in the comment of this answer.
To put my question concretely, the following code should pop from the nested navigator but instead the app exits when the back button is pressed on android.
void main() {
runApp(MaterialApp(title: 'Navigation Basics', home: PearlScreen()));
}
class PearlScreen extends StatefulWidget {
const PearlScreen({Key? key}) : super(key: key);
#override
State<PearlScreen> createState() => _PearlScreenState();
}
class _PearlScreenState extends State<PearlScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => await _navigatorKey.currentState!.maybePop(),
child: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) {
Widget screen;
switch (settings.name) {
case '/':
screen = const HomeScreen();
break;
case '/quiz':
screen = const QuizScreen();
break;
default:
throw "Invalid route in Pearl: ${settings.name}";
}
return MaterialPageRoute(
builder: (_) => Material(child: screen),
);
},
),
);
}
}
But as soon as I change it to onWillPop: () async => !await _navigatorKey.currentState!.maybePop(), it works correctly and pops from the nested navigator instead of exiting the app.
What is the significance of !await?
I wonder how we can call generateRoute without arguments
onGenerateRoute: RouteGenerator.generateRoute
my expectation would be:
RouteGenerator.generateRoute(settings)
Thanks,
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: NumberCreator().stream,
builder: (context, snapshot) {
print('StreamBuilder: ${snapshot.connectionState}');
return MaterialApp(
title: 'Demo',
**onGenerateRoute: RouteGenerator.generateRoute,**
onGenerateInitialRoutes: (String initialRouteName) {
return [
RouteGenerator.generateRoute(
RouteSettings(name: '/', arguments: snapshot)),
];
},
);
});
}
}
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case '/':
if (args is AsyncSnapshot<int>) {
It's not really that you are calling onGenerateRoute without arguments, it is because you are passing a parameters which comply to the expected type RouteFactory defined as follow in the flutter documentation:
typedef RouteFactory = Route<dynamic>? Function(RouteSettings settings);
And your generateRoute comply to this type as it is a function which takes a RouteSettings parameter and returning an object of type Route<dynamic>.
Here is the code I've used in my projects:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
initialRoute: /*<my_initial_route */,
onGenerateRoute: MyRouter.generateRoute,
);
}
}
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
// your cases ...
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
}
What's the correct way of setting up navigation architecture named routes while using a bottomNavigationBar?
Here's my current setup but I feel there's a better way of doing it:
main.dart:
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case NamedRoutes.splashScreen:
return SplashScreen();
case NamedRoutes.login:
return LoginPage();
case NamedRoutes.mainApp:
return NavigatorSetup();
default:
throw Exception('Invalid route: ${settings.name}');
}
});
navigatorSetup.dart:
IndexedStack(
index: Provider.of<RoutesProvider>(context).selectedViewIndex,
children: [FirstMain(), SecondMain(), ThirdMain(), FourthMain()],
), bottomNavigationBar...
in each main files there is the following setup
class FirstMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
key: Provider.of<RoutesProvider>(context).homeKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
case NamedRoutes.mainPage:
return MainPage();
case NamedRoutes.singleMainPage:
return SingleMainPage();
default:
throw Exception('Invalid route: ${settings.name}');
}
},
);
},
);
}
}
Then my routes provider looks like this:
class RoutesProvider extends ChangeNotifier {
int _selectedViewIndex = 0;
get selectedViewIndex => _selectedViewIndex;
set selectedViewIndex(int newIndex) {
_selectedViewIndex = newIndex;
notifyListeners();
}
GlobalKey _mainKey = GlobalKey<NavigatorState>();
GlobalKey _homeKey = GlobalKey();
GlobalKey _secondKey = GlobalKey();
GlobalKey _thirdKey = GlobalKey();
GlobalKey _fourthKey = GlobalKey();
get mainKey => _mainKey;
get homeKey => _homeKey;
get secondKey => _secondKey;
get thirdKey => _thirdKey;
get fourthKey => _fourthKey;
}
The way I'm currently changing routes when on another page of the indexedStack
final RoutesProvider routesProvider = Provider.of<RoutesProvider>(context, listen: false);
final GlobalKey thirdKey = routesProvider.thirdKey;
routesProvider.selectedViewIndex = 2;
Navigator.pushReplacementNamed(thirdKey.currentContext, NamedRoutes.third);
The better way to navigate
Creating a route_generator
import 'package:flutter/material.dart';
import 'package:routing_prep/main.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
// Getting arguments passed in while calling Navigator.pushNamed
final args = settings.arguments;
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case SecondPage.routeName:
// Validation of correct data type
if (args is String) {
return MaterialPageRoute(
builder: (_) => SecondPage(
data: args,
),
);
}
// If args is not of the correct type, return an error page.
// You can also throw an exception while in development.
return _errorRoute();
default:
// If there is no such named route in the switch statement, e.g. /third
return _errorRoute();
}
}
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text('ERROR'),
),
);
});
}
}
As you can see, you've moved from having bits of routing logic everywhere around your codebase, to a single place for this logic - in the RouteGenerator. Now, the only navigation code which will remain in your widgets will be the one pushing named routes with a navigator.
Before you can run and test the app, there's still a bit of a setup to do for this RouteGenerator to function.
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
...
// Initially display FirstPage
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
...
RaisedButton(
child: Text('Go to second'),
onPressed: () {
// Pushing a named route
Navigator.of(context).pushNamed(
SecondPage.routeName,
arguments: 'Hello there from the first page!',
);
},
)
...
}
}
class SecondPage extends StatelessWidget {
static const routeName = "/second";
// This is a String for the sake of an example.
// You can use any type you want.
final String data;
SecondPage({
Key key,
#required this.data,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Routing App'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Second Page',
style: TextStyle(fontSize: 50),
),
Text(
data,
style: TextStyle(fontSize: 20),
),
],
),
),
);
}
}
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')
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