I'm creating a registration form in Flutter, and I would like the user to go through steps. Every step should transition to the next step with a sliding effect. For example, if I am on Step 1, moving to Step 2 should slide the form to the left, and I should get Form 2. Then if I go back to form 1, it should slide the form to the right.
Here's an illustration:
I tried to do that with multiple routes:
routes: {
'/': (context) => HomePage(),
'/step1': (context) => FormStep1(),
'/step2': (context) => FormStep2(),
},
Then on submit:
Navigator.push(
context,
EnterExitRoute(exitPage: FormStep1(), enterPage: FormStep2())
);
EnterExitRoute
But that makes the App Bar slide as well, and I want only the form to slide.
With an advice from a friend, I ended up using PageView. That way I didn't have to make a new route for every step.
class _RegisterFormState extends State<RegisterForm> {
final _formsPageViewController = PageController();
List _forms;
#override
Widget build(BuildContext context) {
_forms = [
WillPopScope(
onWillPop: () => Future.sync(this.onWillPop),
child: Step1Container(),
),
WillPopScope(
onWillPop: () => Future.sync(this.onWillPop),
child: Step2Container(),
),
];
return Expanded(
child: PageView.builder(
controller: _formsPageViewController,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return _forms[index];
},
),
);
}
void _nextFormStep() {
_formsPageViewController.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
bool onWillPop() {
if (_formsPageViewController.page.round() ==
_formsPageViewController.initialPage) return true;
_formsPageViewController.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
return false;
}
}
Explanation:
I'm wrapping every form with WillPopScope so "back" button will
affect navigation.
I'm using physics: NeverScrollableScrollPhysics() option on the PageView builder so it will not be affected by a swipe gesture.
On each button of a form step (except last step) I call the _nextFormStep()
method, which moves to the next form.
The child of each WillPopScope() in the list is simply the form / widget you want to be slided.
as an option you can wrap pages with Navigator widget
something like this
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Title')),
body: SafeArea(
child: WillPopScope(
onWillPop: () async => !await _navigatorKey.currentState.maybePop(),
child: Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) => HomePage());
break;
case '/step1':
return CupertinoPageRoute(builder: (context) => FormStep1());
break;
case '/step2':
return CupertinoPageRoute(builder: (context) => FormStep2());
break;
}
},
),
),
),
),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green[200],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('HomePage'),
RaisedButton(
onPressed: () => Navigator.pushNamed(context, '/step1'),
child: Text('Start'),
),
],
),
);
}
}
class FormStep1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue[200],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('FormStep1'),
RaisedButton(
onPressed: () => Navigator.pushNamed(context, '/step2'),
child: Text('Next'),
),
],
),
);
}
}
class FormStep2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow[200],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('FormStep2'),
RaisedButton(onPressed: () {}, child: Text('Next')),
],
),
);
}
}
also instead of CupertinoPageRoute you can use any custom Route with any transition
Related
There are lots of widgets like Visibility or AnimatedOpacity, but I want a widget to appear and grow to its full size with a smooth animation, moving the other Column's children around it apart.
AnimatedContainer would be cool, but I don't want to set the child's constraints (e.g. height) from the parent (then I would have to test and set the correct size on every UI change).
Use Hero Animation check this link to learn:
https://docs.flutter.dev/development/ui/animations/hero-animations
Example :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/first',
routes: {
'/first': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
);
}
}
class FirstScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: Padding(
padding: EdgeInsets.all(15),
child: Column(
children: [
Hero(
tag: "HeroOne",
child: Icon(
Icons.image,
size: 50.0,
),
),
ElevatedButton(
child: Text('Go to second screen'),
onPressed: () {
Navigator.push(context, CustomPageRoute(SecondScreen()));
},
),
],
),
),
);
}
}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Hero(
tag: "HeroOne",
child: Icon(
Icons.image,
size: 150.0,
),
),
ElevatedButton(
child: Text('Back to first screen!'),
onPressed: () {
Navigator.pop(context);
},
),
],
)
),
);
}
}
class CustomPageRoute<T> extends PageRoute<T> {
final Widget child;
CustomPageRoute(this.child);
#override
Color get barrierColor => Colors.black;
#override
String get barrierLabel => '';
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(seconds: 2);
#override
Widget buildPage({1}
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation
) {
return FadeTransition(
opacity: animation,
child: child,
);
}
}
For More Check :
https://www.woolha.com/tutorials/flutter-creating-hero-transition-examples
While researching more for my own question, I found the AnimatedSize widget which does exactly what I need:
To dynamically show and hide a widget with a size animation, just wrap it in AnimatedSize() and give it a duration.
https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html
In Flutter, I want to make screens like with Fragment in android, in this my code i try to replace each screens into current screen like with Fragment.replecae in android, i used Hook and Provider and my code work fine when in click on buttons to switch between them but i can't implementing back stack, which means when i click on Back button on phone, my code should show latest screen which i stored into _backStack variable, each swtich between this screens i stored current screen index into the this variable.
how can i solve back from this stack in my sample code?
// Switch Between screens:
DashboardPage(), UserProfilePage(), SearchPage()
-------------> -------------> ------------->
// When back from stack:
DashboardPage(), UserProfilePage(), SearchPage()
Exit from application <-------------- <---------------- <-----------
i used Hook and i want to implementing this action with this library features
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MultiProvider(providers: [
Provider.value(value: StreamBackStackSupport()),
StreamProvider<homePages>(
create: (context) =>
Provider.of<StreamBackStackSupport>(context, listen: false)
.selectedPage,
)
], child: StartupApplication()));
}
class StartupApplication extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final List<Widget> _fragments = [
DashboardPage(),
UserProfilePage(),
SearchPage()
];
List<int> _backStack = [0];
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: WillPopScope(
// ignore: missing_return
onWillPop: () {
customPop(context);
},
child: Container(
child: Column(
children: <Widget>[
Consumer<homePages>(
builder: (context, selectedPage, child) {
_currentIndex = selectedPage != null ? selectedPage.index : 0;
_backStack.add(_currentIndex);
return Expanded(child: _fragments[_currentIndex]);
},
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenDashboard),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenProfile),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenSearch),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
void navigateBack(int index) {
useState(() => _currentIndex = index);
}
void customPop(BuildContext context) {
if (_backStack.length - 1 > 0) {
navigateBack(_backStack[_backStack.length - 1]);
} else {
_backStack.removeAt(_backStack.length - 1);
Provider.of<StreamBackStackSupport>(context, listen: false)
.switchBetweenPages(homePages.values[_backStack.length - 1]);
Navigator.pop(context);
}
}
}
class UserProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}
enum homePages { screenDashboard, screenProfile, screenSearch }
class StreamBackStackSupport {
final StreamController<homePages> _homePages = StreamController<homePages>();
Stream<homePages> get selectedPage => _homePages.stream;
void switchBetweenPages(homePages selectedPage) {
_homePages.add(homePages.values[selectedPage.index]);
}
void close() {
_homePages.close();
}
}
TL;DR
The full code is at the end.
Use Navigator instead
You should approach this problem differently. I could present you with a solution that would work with your approach, however, I think that you should instead solve this by implementing a custom Navigator as this is a built-in solution in Flutter.
When you are using a Navigator, you do not need any of your stream-based management, i.e. you can remove StreamBackStackSupport entirely.
Now, you insert a Navigator widget where you had your Consumer before:
children: <Widget>[
Expanded(
child: Navigator(
...
),
),
Container(...), // Your bottom bar..
]
The navigator manages its routes using strings, which means that we will need to have a way to convert your enum (which I renamed to Page) to Strings. We can use describeEnum for that and put that into an extension:
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
Now, you can get the string representation of a page using e.g. Page.screenDashboard.route.
Furthermore, you want to map your actual pages to your fragment widgets, which you can do like this:
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
...
To access the Navigator, we need to have a GlobalKey. Usually we would have a StatefulWidget and manage the GlobalKey like that. Since you want to use flutter_hooks, I opted to use a GlobalObjectKey instead:
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
...
Now, you can use navigatorKey.currentState anywhere in your widget to access this custom navigator. The full Navigator setup looks like this:
Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere((element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings, builder: (context) => _fragments[page]);
},
)
As you can see, we pass the navigatorKey created before and define an initialRoute, making use of the route extension we created. In onGenerateRoute, we find the Page enum entry corresponding to the route name (a String) and then return a MaterialPageRoute with the appropriate _fragments entry.
To push a new route, you simply use the navigatorKey and pushNamed:
onPressed: () => navigatorKey.currentState.pushNamed(Page.screenDashboard.route),
Back button
We also need to customly call pop on our custom navigator. For this purpose, a WillPopScope is needed:
WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: ..,
)
Access the custom navigator inside of the nested pages
In any page that is passed to onGenerateRoute, i.e. in any of your "fragments", you can just call Navigator.of(context) instead of using the global key. This is possible because these routes are children of the custom navigator and thus, the BuildContext contains that custom navigator.
For example:
// In SearchPage
Navigator.of(context).pushNamed(Page.screenProfile.route);
Default navigator
You might be wondering how you can get access to the MaterialApp root navigator now, e.g. to push a new full screen route. You can use findRootAncestorStateOfType for that:
context.findRootAncestorStateOfType<NavigatorState>().push(..);
or simply
Navigator.of(context, rootNavigator: true).push(..);
Here is the full code:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(StartupApplication());
}
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
class StartupApplication extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
return WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: Container(
child: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere(
(element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings,
builder: (context) => _fragments[page]);
},
),
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenDashboard.route),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenProfile.route),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenSearch.route),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
}
class UserProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}
Is it possible to return to the exact same place meaning state wise in flutter while using this?
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new ConnectHome(user:widget.user))));
We have cards on the home screen "ConnectHome()" and we need to return them to the same spot.
You can copy paste run full code below
You can await Navigator.push and In Navigator.pop include UserObject()
You can see the code continue execution and print UserObject()
code snippet
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => ConnectHome()),
);
print('result ${result.name}')
...
Navigator.pop(context, UserObject("hello","world"));
working demo
full code
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Returning Data',
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Returning Data Demo'),
),
body: Center(child: SelectionButton()),
);
}
}
class SelectionButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: Text('Pick an option, any option!'),
);
}
// A method that launches the SelectionScreen and awaits the result from
// Navigator.pop.
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => ConnectHome()),
);
print('result ${result.name}');
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result.
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("${result.name}")));
}
}
class UserObject {
String name;
String id;
UserObject(this.name, this.id);
}
class ConnectHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Yep!" as the result.
Navigator.pop(context, UserObject("hello","world"));
},
child: Text('Hello'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Nope!" as the result.
Navigator.pop(context, UserObject("no","No"));
},
child: Text('No.'),
),
)
],
),
),
);
}
}
Does anyone have any recommendations for figuring out nested navigation in Flutter?
What I want is to keep a persistent BottomNavigationBar even when redirecting to new screens. Similar to YouTube, where the bottom bar is always there, even when you browse deeper into the menus.
I'm unable to figure it out from the docs.
The only tutorial I have been able to find so far that goes in-depth into exactly my requirement is https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf (source code). However, It's super confusing.
Right now I'm using
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return Container()
However, its just pushing the new widget over the entire stack, covoring the BottomNavigationBar.
Any tips would be greatly appreciated!
Here is a simple example that even supports popping to the first screen with a tab bar.
import 'package:flutter/material.dart';
import '../library/screen.dart';
import '../playlists/screen.dart';
import '../search/screen.dart';
import '../settings/screen.dart';
class TabsScreen extends StatefulWidget {
#override
_TabsScreenState createState() => _TabsScreenState();
}
class _TabsScreenState extends State<TabsScreen> {
int _currentIndex = 0;
final _libraryScreen = GlobalKey<NavigatorState>();
final _playlistScreen = GlobalKey<NavigatorState>();
final _searchScreen = GlobalKey<NavigatorState>();
final _settingsScreen = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: <Widget>[
Navigator(
key: _libraryScreen,
onGenerateRoute: (route) => MaterialPageRoute(
settings: route,
builder: (context) => LibraryScreen(),
),
),
Navigator(
key: _playlistScreen,
onGenerateRoute: (route) => MaterialPageRoute(
settings: route,
builder: (context) => PlaylistsScreen(),
),
),
Navigator(
key: _searchScreen,
onGenerateRoute: (route) => MaterialPageRoute(
settings: route,
builder: (context) => SearchScreen(),
),
),
Navigator(
key: _settingsScreen,
onGenerateRoute: (route) => MaterialPageRoute(
settings: route,
builder: (context) => SettingsScreen(),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
onTap: (val) => _onTap(val, context),
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.library_books),
title: Text('Library'),
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
title: Text('Playlists'),
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text('Settings'),
),
],
),
);
}
void _onTap(int val, BuildContext context) {
if (_currentIndex == val) {
switch (val) {
case 0:
_libraryScreen.currentState.popUntil((route) => route.isFirst);
break;
case 1:
_playlistScreen.currentState.popUntil((route) => route.isFirst);
break;
case 2:
_searchScreen.currentState.popUntil((route) => route.isFirst);
break;
case 3:
_settingsScreen.currentState.popUntil((route) => route.isFirst);
break;
default:
}
} else {
if (mounted) {
setState(() {
_currentIndex = val;
});
}
}
}
}
Here is the example code for persistent BottomNavigationBar as a starter:
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MainPage(),
);
}
}
class MainPage extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: navigatorKey,
onGenerateRoute: (route) => MaterialPageRoute(
settings: route,
builder: (context) => PageOne(),
),
),
),
BottomNavigationBar(navigatorKey)
],
),
);
}
}
class BottomNavigationBar extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
BottomNavigationBar(this.navigatorKey) : assert(navigatorKey != null);
Future<void> push(Route route) {
return navigatorKey.currentState.push(route);
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: ButtonBar(
alignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
RaisedButton(
child: Text("PageOne"),
onPressed: () {
push(MaterialPageRoute(builder: (context) => PageOne()));
},
),
RaisedButton(
child: Text("PageTwo"),
onPressed: () {
push(MaterialPageRoute(builder: (context) => PageTwo()));
},
)
],
),
);
}
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Page One"),
RaisedButton(
onPressed: (){
Navigator.of(context).pop();
},
child: Text("Pop"),
),
],
),
);
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Page Two"),
RaisedButton(
onPressed: (){
Navigator.of(context).pop();
},
child: Text("Pop"),
),
],
),
);
}
}
Here is how it the screen record
I m doing a Flutter app and I would like to go back from page 4 to page 1.
I have an error really strange :
Bad state : Future already completed
I created a simple project to reproduce this bug :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
onGenerateRoute: routes,
);
}
}
Route routes(RouteSettings settings) {
if (settings.name == '/page1') {
return MaterialPageRoute(
builder: (context) {
return Page1();
},
);
} else if (settings.name == '/page2') {
return MaterialPageRoute(
builder: (context) {
return Page2();
},
);
} else if (settings.name == '/page3') {
return MaterialPageRoute(
builder: (context) {
return Page3();
},
);
} else if (settings.name == '/page4') {
return MaterialPageRoute(
builder: (context) {
return Page4();
},
);
} else {
return MaterialPageRoute(
builder: (context) {
return Page1();
},
);
}
}
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Page 1'),
RaisedButton(
child: Text('Go Page 2'),
onPressed: () {
Navigator.of(context).pushNamed('/page2');
},
)
],
),
),
);
}
}
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Page 2'),
RaisedButton(
child: Text('Go Page 3'),
onPressed: () {
Navigator.of(context).pushNamed('/page3');
},
)
],
),
),
);
}
}
class Page3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Page 3'),
RaisedButton(
child: Text('Go Page 4'),
onPressed: () {
Navigator.of(context).pushNamed('/page4');
},
)
],
),
),
);
}
}
class Page4 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Page 4'),
RaisedButton(
child: Text('Go Back Page 1'),
onPressed: () {
Navigator.of(context).popUntil(ModalRoute.withName('/page1'));
},
)
],
),
),
);
}
}
How can I solve that ?
Instead of:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Page1(),
),
);
use:
Navigator.of(context).push(
MaterialPageRoute(
settings: RouteSettings(name: "/Page1"),
builder: (context) => Page1(),
),
);
and then you can use :
Navigator.of(context)
.popUntil(ModalRoute.withName("/Page1"));
It is duplicate question. Refer this and this.
Basically what is happening is - when you start your app, page1 opens because it goes into the last else and there is no name assigned to it, so when you do popuntil that page name, it doesn't find it at all.
For page_transition plugin
In case you are using named Routes and this Package for transitions between pages (and arguments):
MaterialApp(
onGenerateRoute: (settings) => {
switch (settings.name) {
case "/yourRoute":
final value = settings.arguments as String?; // only if you pass arguments
return PageTransition(
settings: RouteSettings(
name: "/yourRoute", //HERE is where you name your route for using popUntil
),
child: YourPage(
parameters: value ?? "null",
),
type: PageTransitionType.fade,
);
case "/yourNextRoute":
...
}
}
),
Edit your main like this to enable calling and using pop with named routes. This would look like this:
Navigator.popUntil(context, ModalRoute.withName("/yourRoute")) or Navigator.pushNamed(context, "/yourRoute",arguments: "12345")