I want to push two pages to the flutter navigator one after another, so that going back from 2nd page redirects me to the first page. The code for this action will look somewhat like below -
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
The above code works fine. But my confusion is, will it work always as the pushNamed function is asynchronous as it returns a future value. So it could happen that the second page got pushed to navigator before the first page.
The ideal implementation seems to me to wait for the first call to pushNamed return its value and then call the second one. But strangely the following two solutions don't work. The first page did get pushed but it doesn't push the second page.
Solution 1(Not working):
Navigator.of(context).pushNamed(
FirstPage.PATH.then((_) =>
Navigator.of(context).pushNamed(SecondPage.PATH));
Solution 2(Not working):
await Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
Can anyone please clarify what I'm thinking wrong? Any help will be much appreciated. Thanks in Advance!
As an option you can pass a callback to pageA, add animation listener and call this callback when animation is finished.
this is full example:
import 'package:flutter/material.dart';
main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
routes: {
'pageA': (context) => PageA(),
'pageB': (context) => PageB(),
},
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _onPressed,
child: Text('press me'),
),
),
);
}
void _onPressed() {
Navigator.of(context).pushNamed(PageA.routeName, arguments: _pushNextPage);
}
void _pushNextPage() {
Navigator.of(context).pushNamed(PageB.routeName);
}
}
class PageA extends StatefulWidget {
static const routeName = 'pageA';
#override
_PageAState createState() => _PageAState();
}
class _PageAState extends State<PageA> {
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
ModalRoute.of(context)?.animation?.addStatusListener(_statusListener);
});
}
void _statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
final route = ModalRoute.of(context);
route?.animation?.removeStatusListener(_statusListener);
final callback = route?.settings.arguments as VoidCallback;
callback.call();
}
}
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE A')));
}
class PageB extends StatelessWidget {
static const routeName = 'pageB';
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE B')));
}
Your solutions do not work, because the Future returned by pushNamed is only completed when the page is removed from the navigation stack again.
So in your examples, the second page is pushed, once the first page has been closed.
I don't think it can happen, that the second page will be pushed before the first page in this example:
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
This should be a valid solution for what you want to achieve.
Related
Minimal reproducible code:
void main() => runApp(FooApp());
class FooApp extends StatefulWidget {
#override
_FooAppState createState() => _FooAppState();
}
class _FooAppState extends State<FooApp> {
bool _showPage2 = false;
void _onPressed(bool value) => setState(() => _showPage2 = value);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Navigator(
onPopPage: (route, result) => route.didPop(result),
pages: [
MaterialPage(child: Page1(onPressed: _onPressed)),
if (_showPage2) MaterialPage(child: Page2()),
],
),
);
}
}
class Page1 extends StatelessWidget {
final ValueChanged<bool> onPressed;
const Page1({Key key, this.onPressed}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page1')),
body: ElevatedButton(
onPressed: () => onPressed(true),
child: Text('Page2'),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
void dispose() {
super.dispose();
print('dispose');
}
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}
didPop:
When this function returns true, the navigator removes this route from the history but does not yet call dispose. Instead, it is the route's responsibility to call NavigatorState.finalizeRoute, which will in turn call dispose on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
But in my example, you can see without having to call NavigatorState.finalizeRoute in Page2, dispose method does get called contradiciting Docs.
It's done internally when using MaterialPage/_PageBasedMaterialPageRoute. You can poke around in the code starting in the Navigator class, which appears to lead up to this OverlayRoute class. If you do want to trace through yourself, it wasn't a walk in the park for me and you'll have to pay close attention to how each class is related.
This class has the finishedWhenPopped getter, which is true by default. And if you look at the didPop override implementation right below the getter definition, didPop will internally call finalizeRoute when finishedWhenPopped is true.
Implementation from OverlayRoute class
#protected
bool get finishedWhenPopped => true;
#override
bool didPop(T? result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
if (finishedWhenPopped)
navigator!.finalizeRoute(this);
return returnValue;
}
This is true only for at least MaterialPage/_PageBasedMaterialPageRoute. Other implementations don't necessarily do this.
Hello I'm creating an app with multiple pages using Navigators and routes.
I would like to add to the Scaffold Appbar a counter that increment every time a finger clicks on a screen button (also if they are more then one button present in the page).
Also if I change the pages, this counter must increase.
Can you help me to understand the issue?
I'm learing so probably the structure could be a "beginner" version.
Thanks.
As #Randal Schwartz commented, we could take advantage of the onGenerateRoute property of the MaterialApp class:
runApp(MaterialApp(
home: BuilderPage(LoginArguments(false)),
onGenerateRoute: generateRoute
));
By defining a custom generateRoute function:
int counter = 0; // global variable outside of the classes
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case 'home':
counter++;
return MaterialPageRoute(builder: (_) => HomeScreen());
case 'login':
counter++;
return MaterialPageRoute(builder: (_) => LoginScreen());
case 'register':
counter++;
return MaterialPageRoute(builder: (_) => RegisterScreen());
}
}
The counter could be then displayed by the Widgets by loading it in their state:
class HomeScreen extends StatefulWidget {
const HomeScreen({ Key key }) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _counter;
#override
void initState() {
super.initState();
setState(() => _counter = counter);
}
void _increaseCounter() {
counter++;
setState(() => _counter++);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(child: Text(_counter.toString())),
body: SafeArea(
child: Center(child:
GestureDetector(
onTap: () { _increaseCounter(); },
child: Text('test'),
),
),
),
),
);
}
}
NB: this solution is not optimal. Please make sure to consider more advanced state management architectures, like BLoC, to better manage the data syncronization between different Widgets in the app.
You can probably hook some logging into the PageRouteBuilder, because every route goes through there from a given page (of course, it'd have to be on every page too).
My goal is to track routes being presented and dismissed for analytics purposes like described on this article: https://medium.com/flutter-community/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea
I am pushing a CupertinoPageRoute and passing a Widget to it that implements the RouteAware Mixin.
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
return CupertinoApp(
...
navigatorObservers: [routeObserver],
}
Pushing like so:
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => MyWidget()),
);
The Widget
class MyWidget extends StatefulWidget {
MyWidget({Key key}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPop() {
print('never called');
}
#override
void didPopNext() {
print('never called');
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
CupertinoNavigationBar(
middle: Text('42'),
),
],
),
);
}
}
The didPop() method is never called when popping a CupertinoPageRoute with the back button unless I push with rootNavigator = true which is not what I want.
How can I fix this?
EDIT: This behavior only happens when pushing routes from a CupertinoTabView, as it creates a new nested navigator.
Github Issue
If I understand correctly, the final solution shown in the Medium article does not extend every StatefulWidget with RouteAware. Instead, it extents its own implementation of a RouteObserver to get notified about every route change.
Using this approach, you should be able to insert your custom RouteObserver into both you main App as well as your CupertinoTabView to achieve what you need.
I have two Pages moving each others like this.
MyHomePage -> ResPage -> MyHomePage -> ResPage
I want to exec the function when every time the ResPage appears.
code is this
Navigator.push(context,MaterialPageRoute(
builder: (context) => ResPage()));
Navigator.push(context,MaterialPageRoute(
builder: (context) => MyHomePage())
resPage is Stateful Widget.
class ResPage extends StatefulWidget {
ResPage({Key key}) : super(key: key);
#override
_ResPageState createState() => _ResPageState();
}
class _ResPageState extends State<ResPage> {
void initState(){ // it called just once.
}
#override
Widget build(BuildContext context) {
return Stack( // it called many times.
children: <Widget>[
background,
Scaffold(
backgroundColor: Colors.transparent,
body: resStack
),
],
);
}
initState() is called one time, and build is called many times.
Is there any call back when the page appears??
Yes it is!
You can add a post frame callback(a call after the widget was built)
like this:
Widget build(BuildContext context) {
WidgetsBinding.addPostFrameCallback(functionToCall);
}
There are two ways to change what a user sees on display: I can push to another page or I can change the state of my stateful widget and rebuild it. Can you tell me, which way is best practice? (And if it depends - what I guess - on what?)
Pushing:
class Pushing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () => Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage())),)
),
);
}
}
Using States
class UsingStates extends StatefulWidget {
#override
State createState() => new _UsingStatesState();
}
class _UsingStatesState extends State<UsingStates> {
bool isPageTwo;
#override
void initState() {
isPageTwo = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isPageTwo ? Center(child: Text('Page two')) : Center(child: RaisedButton(onPressed: () {
setState(() {
isPageTwo = true;
});
})),
);
}
}
Of course the answer is: It depends.
When to use Navigator:
Routes pushed with the navigator are in a way equivalent to...
Activity and Fragment in Android
A route in Angular or React
A html file of a classic web page
You would use the Navigator to switch between logically separate parts of your app. Think of a StackOverflow app that comes with different pages, e.g. "Question List", "Question Detail", "Ask Question" form and "User Profile".
Navigator takes care of back navigation (hardware back button on android phones + back arrow in AppBar)
Note that a route does not have to overlay the full screen. showDialog also uses Navigator.push() internally (that's why you use Navigator.pop() to dismiss a dialog.
Similar to startActivityForResult on Android, a route can also return a result when pop is called. Think of a dialog that lets you pick a date.
When to use State:
Use State when the screens are a logical unit, e.g.:
When you load a list of items from a server, you would have 4 different states:
Loading
"An error occured..."
Placeholder displayed when the list is empty
The ListView
A form with multiple steps
A screen with multiple tabs (in this case the navigations is handled by the tab bar)
A "Please wait" overlay that blocks the screen while a POST request is sent to the server
After all Navigator is also a StatefulWidget that keeps track of the route history. Stateful widgets are a fundamental building block of Flutter. When your app is really complex and Navigator does not fit your needs, you can always create your own StatefulWidget for full control.
It always helps to look at Flutter's source code (CTRL + B in Android Studio).
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Page1(title: 'Flutter Demo Home Page'),
);
}
}
class Page1 extends StatefulWidget {
Page1({Key key, this.title}) : super(key: key);
final String title;
#override
Page1State createState() => new Page1State();
}
class Page1State extends State<Page1> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 1, Press here to go to page 2"),onPressed:()=>streamController.add(2) ,),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 1 stream : "+intValue.toString());
if(intValue==2){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page2(title: "Page 2",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
class Page2 extends StatefulWidget {
Page2({Key key, this.title}) : super(key: key);
final String title;
#override
Page2State createState() => new Page2State();
}
class Page2State extends State<Page2> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 2, Press here to go to page 2"),onPressed:()=> streamController.add(1),),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 2 stream : "+intValue.toString());
if(intValue==1){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page1(title: "Page 1",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
Sorry for bad formatting. You can run this code without any dependencies. Hope it helped