restorablePush then pushAndRemoveUntil results in error (flutter) - flutter

What I want to achieve:
Screen 1 is the initial screen, and goes to Screen 2 (via restorablePush)
Screen 2 is a restorable page, and goes to Screen 3 (via restorablePush)
Screen 3 is a restorable page, and goes back to Screen 1 (via pushAndRemoveUntil)
The restoring stuff works fine, but when I go back to Screen 1 from Screen 3 then kill the app and reopen it, I get this failed assertion error:
_history.isNotEmpty:
All routes returned by onGenerateInitialRoutes are not restorable.
Please make sure that all routes returned by onGenerateInitialRoutes
have their RouteSettings defined with names that are defined in the
app's routes table.
I've look at onGeneralInitialRoutes but I can't figure out how to solve this. I also tried doing everything with named routes, but it didn't change anything.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: Screen1(),
restorationScopeId: 'root',
);
}
}
class Screen1 extends StatefulWidget {
const Screen1({Key? key}) : super(key: key);
#override
State<Screen1> createState() => _Screen1State();
}
class _Screen1State extends State<Screen1> {
static Route<void> _myRouteBuilder(BuildContext context, Object? arguments) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => const Screen2(),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: TextButton(
onPressed: () {
Navigator.restorablePush(context, _myRouteBuilder);
},
child: const Text('Go to Screen 2')),
),
);
}
}
class Screen2 extends StatefulWidget {
const Screen2({Key? key}) : super(key: key);
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
static Route<void> _myRouteBuilder(BuildContext context, Object? arguments) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => const Screen3(),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen 2')),
body: Center(
child: TextButton(
onPressed: () => Navigator.restorablePush(context, _myRouteBuilder),
child: const Text('Go to Screen 3'),
)));
}
}
class Screen3 extends StatelessWidget {
const Screen3({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Screen 3')),
body: Center(
child: TextButton(
onPressed: () => Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const Screen1()),
((route) => false)),
child: const Text('Go back to Screen 1'),
),
));
}
}

Related

Flutter navigate to second screen and focus on Text field?

When I tap on a button I want to navigate to the second screen Focus on TextField(there is only one) and raise a keyboard.
I successfully focused when I taped on a widget on the same screen using FocusNode.
Use autofocus: true,
TextField(autofocus: true,
First Page :
class firstPage extends StatefulWidget {
firstPage({Key? key}) : super(key: key);
#override
State<firstPage> createState() => _firstPageState();
}
class _firstPageState extends State<firstPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("First Page")),
body: Center(
child: ElevatedButton(
child: Text("go to second page"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(
focusOnTextField: false,
)),
);
}),
));
}
}
Second page :
class SecondPage extends StatefulWidget {
SecondPage({Key? key, this.focusOnTextField}) : super(key: key);
bool? focusOnTextField;
#override
State<SecondPage> createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Second Page")),
body: Center(
child: TextField(
autofocus: widget.focusOnTextField ?? false,
)),
);
}
}

How can I resolve this Flutter Navigator.push() error?

I am trying to move from one page to another via Navigator.push. Here is my code:
import 'package:flutter/material.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> {
int count = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrange,
title: const Text("Noice!"),
),
body: ElevatedButton(
child: const Text('About'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AboutScreen(),
),
);
},
),
class AboutScreen extends StatelessWidget {
const AboutScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 0, 255, 8),
title: const Text("Flutter ballert")),
);
}
}
The code compiles and I can access the app, but clicking the button leads to an error:
Exception has occurred.
FlutterError (Navigator operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.)
I know that in the offical docs the example for navigation is a tiny bit different. They dont return MaterialApp, instead they return Scaffold. Whats the difference between the two? Why should I return MaterialApp in the first place? And why doesnt it work with returning MaterialApp?
I dont know why this happens. I am new to flutter, so apologies if thats really trivial.
EDIT: Duplicate of:
Navigator operation requested with a context that does not include a Navigator
The problem is with the context. To fix this. Wrap your widget in a Builder() Class:
import 'package:flutter/material.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> {
int count = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(builder: (context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrange,
title: const Text("Noice!"),
),
body: ElevatedButton(
child: const Text('About'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AboutScreen(),
));
},
),
);
}),
);
}
}
class AboutScreen extends StatelessWidget {
const AboutScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 0, 255, 8),
title: const Text("Flutter ballert")),
);
}
}
Here is a YouTube video by the Google team explaining Builder
You need to pass the context:
import 'package:flutter/material.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> {
int count = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrange,
title: const Text("Noice!"),
),
body: ElevatedButton(
child: const Text('About'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => AboutScreen(),
),
);
},
),
),
);
}
}
class AboutScreen extends StatelessWidget {
const AboutScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 0, 255, 8),
title: const Text("Flutter ballert")),
);
}
}
Basically you did:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AboutScreen(),
),
);
But you need to do this:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => AboutScreen(),
),
);

Show Snackbar on top route after closing a page

In my app, I have some areas where I can open a new page on top of the current, that allow to edit data. Once editing is done, I want to close the page (i.e. via Navigator.pop(context);), and also show a Snackbar after closing (i.e. via ScaffoldMessenger.of(context).showSnackBar('X has been saved')). I am using a ScaffoldMessenger for that.
However, if after closing the edit-page only the top-route remains, the Snackbar will not be shown. If I open any other page fast enough, it will be shown there for the remaining time though. So it was triggered, it is just not shown on the top-route. Also, if I open the edit-page not from the top-route, but from any other page that was already opened on top, the Snackbar will show normally after closing the edit-page.
If I open a Snackbar directly on the top-route, it also works fine. So instead of opening the Snackbar from the edit-page, I could technically return the message and then trigger the Snackbar. But I would prefer not to to pass data around and call functionality at several places, but just call the method at one place (where it belongs).
I can reproduce this behaviour on a newly created App, just need to replace the _MyHomePageState with the following code. What am I doing wrong here?
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<ScaffoldMessengerState> _globalScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
#override
Widget build(BuildContext context) {
return ScaffoldMessenger(
key: _globalScaffoldMessengerKey,
child: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push<bool>(context, MaterialPageRoute(builder: (context) => const SubPage()));
},
child: const Text("Open Subpage"),
),
),
),
);
}
}
class SubPage extends StatelessWidget {
const SubPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Hello Snackbar')));
},
child: const Text("Close Subpage"),
),
),
);
}
}
Remove the scaffold Messenger widget from the first page
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => const SubPage()));
},
child: const Text("Open Subpage"),
),
),
);
}
}
class SubPage extends StatelessWidget {
const SubPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Hello Snackbar')));
},
child: const Text("Close Subpage"),
),
),
);
}
}
I checked this code and it shows snackbar in the page that exists after popping the subpage

Back to view in flutter

i have a little problem with the Navigator in flutter. I have 3 windows: (Login -> Home -> Orders). But when I go from Login to Home, everything works fine, but if I go from Home to Orders and use the android back button, it returns me to the Login window, that is, until the first view, not the second.
My code Navigation of Login:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => HomeScreen(),
),
);
My Code Navigation of HomeScreen
Navigator.push(this.context,
MaterialPageRoute(
builder: (context) =\> Orders(
numTable: numTable,
),
)
);
Solution : use pushAndRemoveUntil or pushReplacement at the LoginPage
class LoginPage extends StatelessWidget {
const LoginPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: InkWell(
onTap: ()=>Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => HomePage(),
)
,(Route<dynamic> route) => false), child: Center(child: Text("LoginPage"),)),
);
}
}
------------
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: InkWell(
onTap: ()=>Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => OrdersPage(),
))
, child: Center(child: Text("HomePage"),)),
);
}
}
---------------
class OrdersPage extends StatelessWidget {
const OrdersPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text("OrdersPage"),),
);
}
}
if users login successfully use in Login pushReplacement
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => HomeScreen(),
),
);
in HomeScreen
Navigator.push(this.context,
MaterialPageRoute(
builder: (context) =\> Orders(
numTable: numTable,
),
)
);
when click android back button will go window HomeScreen
You can follow this approach which does what you want, just do Navigator.of(context).push(route) on each page:
class Login extends StatefulWidget {
const Login({Key? key}) : super(key: key);
#override
State<Login> createState() => _LoginState();
}
class _LoginState extends State<Login> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Center(
child: Column(
children: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Home(),
),
);
},
child: const Text('HOME'),
),
],
),
),
);
}
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Column(
children: [
TextButton(onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Orders(),
),
);
}, child: const Text('Orders'))
],
),
);
}
}
class Orders extends StatefulWidget {
const Orders({Key? key}) : super(key: key);
#override
State<Orders> createState() => _OrdersState();
}
class _OrdersState extends State<Orders> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Orders'),
),
body: Column(
children: [
TextButton(onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Home(),
),
);
}, child: const Text('Home'))
],
),
);
}
}
Although there are answers to your question using the Navigator from the Material package, I would like to provide you with a tip for much simpler navigation in Flutter: Use the Get package.
With Get, this code:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => HomeScreen(),
),
);
can be replaced with this code:
Get.to(() => HomeScreen());
In your example, you will then use the following code to go from Login to Home when a user has been authenticated:
Get.offAll(() => HomeScreen());
After that, you can move from screen to screen like this:
Get.off(() => [targetscreen]());
Get.to(() => [targetscreen]());

Flutter Provider nested navigation

I have a problem with provider and navigation.
I have a HomeScreen with a list of objects. When you click on one object I navigate to a DetailScreen with tab navigation. This DetailScreen is wrapped with a ChangenotifierProvider which provides a ViewModel
Now, when I navigate to another screen with Navigator.of(context).push(EditScreen) I can't access the ViewModel within the EditScreen
The following error is thrown
════════ Exception caught by gesture ═══════════════════════════════════════════
The following ProviderNotFoundException was thrown while handling a gesture:
Error: Could not find the correct Provider<ViewModel> above this EditScreen Widget
This is a simple overview of what I try to achieve
Home Screen
- Detail Screen (wrapped with ChangeNotifierProvider)
- Edit Screen
- access provider from here
I know what the problem is. I'm pushing a new screen on the stack and the change notifier is not available anymore.
I thought about creating a Detail Repository on top of my App which holds all of the ViewModels for the DetailView.
I know I could wrap the ChangeNotifier around my MaterialApp, but I don't want that, or can't do it because I don't know which Detail-ViewModel I need. I want a ViewModel for every item in the list
I really don't know what's the best way to solve this. Thanks everyone for the help
Here is a quick example app:
This is a picture of the image tree
import 'package:flutter/material.dart';
import 'package:provider/provider.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',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("DetailView"),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider(
create: (_) => ViewModel(), child: DetailScreen()))),
)));
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("EditScreen"),
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => EditScreen())),
),
));
}
}
class EditScreen extends StatelessWidget {
const EditScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("Print"),
onPressed: () =>
Provider.of<ViewModel>(context, listen: false).printNumber()),
),
);
}
}
class ViewModel extends ChangeNotifier {
printNumber() {
print(2);
}
}
To be able to access providers accross navigations, you need to provide it before MaterialApp as follows
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ViewModel(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("DetailView"),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailScreen(),
),
),
)));
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("EditScreen"),
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => EditScreen())),
),
));
}
}
class EditScreen extends StatelessWidget {
const EditScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("Print"),
onPressed: () =>
Provider.of<ViewModel>(context, listen: false).printNumber()),
),
);
}
}
class ViewModel extends ChangeNotifier {
printNumber() {
print(2);
}
}
A bit late to the party, but I think this is the answer the question was looking for:
(Basically passing the ViewModel down to the next Navigator page.)
class DetailScreen extends StatelessWidget {
const DetailScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final viewModel = Provider.of<ViewModel>(context); // Get current ViewModel
return Scaffold(
body: Center(
child: RaisedButton(
child: Text("EditScreen"),
onPressed: () => Navigator.of(context).push(
// Pass ViewModel down to EditScreen
MaterialPageRoute(builder: (context) {
return ChangeNotifierProvider.value(value: viewModel, child: EditScreen());
}),
),
),
));
}
}
I am a bit late but I found a solution on how to keep the value of a Provider alive after a Navigator.push() without having to put the Provider above the MaterialApp.
To do so, I have used the library custom_navigator. It allows you to create a Navigator wherever you want in the tree.
You will have to create 2 different GlobalKey<NavigatorState> that you will give to the MaterialApp and CustomNavigator widgets. These keys will allow you to control what Navigator you want to use.
Here is a small snippet to illustrate how to do
class App extends StatelessWidget {
GlobalKey<NavigatorState> _mainNavigatorKey = GlobalKey<NavigatorState>(); // You need to create this key for the MaterialApp too
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _mainNavigatorKey; // Give the main key to the MaterialApp
home: Provider<bool>.value(
value: myProviderFunction(),
child: Home(),
),
);
}
}
class Home extends StatelessWidget {
GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(); // You need to create this key to control what navigator you want to use
#override
Widget build(BuildContext context) {
final bool myBool = Provider.of<bool>(context);
return CustomNavigator (
// CustomNavigator is from the library 'custom_navigator'
navigatorKey: _navigatorKey, // Give the second key to your CustomNavigator
pageRoute: PageRoutes.materialPageRoute,
home: Scaffold(
body: FlatButton(
child: Text('Push'),
onPressed: () {
_navigatorKey.currentState.push( // <- Where the magic happens
MaterialPageRoute(
builder: (context) => SecondHome(),
),
},
),
),
),
);
}
}
class SecondHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bool myBool = Provider.of<bool>(context);
return Scaffold(
body: FlatButton(
child: Text('Pop'),
onPressed: () {
Novigator.pop(context);
},
),
);
}
}
Here you can read the value myBool from the Provider in the Home widget but also ine the SecondHome widget even after a Navigator.push().
However, the Android back button will trigger a Navigator.pop() from the Navigator of the MaterialApp. If you want to use the CustomNavigator's one, you can do this:
// In the Home Widget insert this
...
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop(); // Use the custom navigator when available
return false; // Don't pop the main navigator
} else {
return true; // There is nothing to pop in the custom navigator anymore, use the main one
}
},
child: CustomNavigator(...),
);
}
...