showDialog from root widget - flutter

I want to show a dialog from root widget (the one that created MaterialApp) I have a NavigatorState instance, but showDialog requires context that would return Navigator.of(context).
It looks like I need to provide context from a route, but I can't do this, because the root widget does not have it.
EDIT: I have found a workaround: I can push fake route that is only there to showDialog and then pop that route when dialog finishes. Not pretty but works.

I fixed the problem by using navigatorKey.currentState.overlay.context. Here is example:
class GlobalDialogApp extends StatefulWidget {
#override
_GlobalDialogAppState createState() => _GlobalDialogAppState();
}
class _GlobalDialogAppState extends State<GlobalDialogApp> {
final navigatorKey = GlobalKey<NavigatorState>();
void show() {
final context = navigatorKey.currentState.overlay.context;
final dialog = AlertDialog(
content: Text('Test'),
);
showDialog(context: context, builder: (x) => dialog);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
home: Scaffold(
body: Center(
child: RaisedButton(
child: Text('Show alert'),
onPressed: show,
),
),
),
);
}
}

tl;dr: If you want to call showDialog from your root widget, extrude your code into another widget (e.g. a StatelessWidget), and call showDialog there.
Anyway, in the following I'm going to assume you are running into this issue:
flutter: No MaterialLocalizations found.
flutter: MyApp widgets require MaterialLocalizations to be provided by a Localizations widget ancestor.
flutter: Localizations are used to generate many different messages, labels,and abbreviations which are used by the material library.
As said before, showDialog can only be called in a BuildContext whose ancestor has a MaterialApp. Therefore you can't directly call showDialogif you have a structure like this:
- MaterialApp
- Scaffold
- Button // call show Dialog here
In a code example this would result in code like this, throwing the error given above:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(
child: RaisedButton(
child: Text('Show dialog!'),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Text('Dialog.'),
);
});
}),
),
),
);
}
}
To solve this error from occuring you can create a new Widget, which has its own BuildContext. The modified structure would look like this:
- MaterialApp
- Home
- Home // your own (Stateless)Widget
- Button // call show Dialog here
Modifying the code example to the structure given above, results in the code snippet below. showDialogcan be called without throwing the error.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: Home()
);
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Show dialog!'),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Text('Dialog.'),
);
});
}),
),
);
}
}

They changed the way the navigator overlay works.
This is the working solution for us as the accepted one isn't anymore.
// If you want to use the context for anything.
final context = navigatorKey.currentState.overlay.context;
// How to insert the dialog into the display queue.
navigatorKey.currentState.overlay.insert(anyDialog);

If you already have a context object, you can get root material app's context by
final rootContext = context.findRootAncestorStateOfType<NavigatorState>().context
and passing this to showDialog or showModalBottomSheet context argument.

Since showDialog is used for showing a material dialog It can be used for showing dialogs inside a MaterialApp widget only. It can not be used to show dialog outside it.

If it helps anyone else, inject the navigator key into a dialog widget like so.
class MyApp extends StatelessWidget {
MyApp({Key key});
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
onGenerateRoute: Router.generateRoute,
// ...
builder: (context, routeChild) {
return Material(
child: InviteRequestModal(
navigatorKey: navigatorKey,
child: routeChild,
),
);
},
);
}
Then in the Widget that requires the modal, you can use it as mentioned above.
class InviteRequestModal extends StatelessWidget {
final Widget child;
final GlobalKey<NavigatorState> navigatorKey;
InviteRequestModal({
Key key,
this.child,
this.navigatorKey,
}) : super(key: key);
void _showInviteRequest(InviteRequest invite) {
final context = navigatorKey.currentState.overlay.context;
showDialog(
context: context,
builder: (_) {
// Your dialog content
return Container();
}
);
}
#override
Widget build(BuildContext context) {
return BlocListener<InviteContactsBloc, InviteContactsState>(
listenWhen: (previous, current) => current is InviteRequestLoaded,
listener: (_, state) {
if (state is InviteRequestLoaded) {
_showInviteRequest(state.invite);
}
},
child: child,
);
}
}

The answer just that simple, when you are providing MaterialApp to the tree it was providing but at the immediate bottom you are the context which obtained before providing MaterialApp to the tree. To resolve the issue you need to create a new context which will have the MaterialApp properties. For that wrap a Builder above the home and vola it is working...!
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(builder: (context) {
return HomePage(
child: Center(
child: TextButton(
onPressed: () async {
await showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
color: Colors.green,
height: 50,
width: 100,
child: Text("Hi, I am a dialog"),
),
),
);
},
child: Text("Tap me"),
),
),
);
}),
);
}

For those wanting to see how to do this in a multiple widget/route/file scenario, I used it with InheritedWidget and an extension on BuildContext.
main.dart
import 'package:flutter/material.dart';
import 'package:myapp/home_screen.dart';
import 'package:myapp/app_navkey.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return AppNavKey(
navigatorKey: navigatorKey,
child: MaterialApp(
navigatorKey: navigatorKey,
theme: ThemeData(),
home: Scaffold(
body: HomeScreen(),
),
),
);
}
}
home_screen.dart
import 'package:myapp/extensions.dart';
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final overlayContext = context.navigationKey().currentState.overlay.context;
return Center(
child: TextButton(
child: Text('Show dialog!'),
onPressed: () {
showDialog(
context: overlayContext, // use app level navigation context overlay
builder: (BuildContext context) {
return Dialog(
child: Text('Dialog.'),
);
});
},
),
);
}
}
app_navkey.dart
import 'package:flutter/widgets.dart';
class AppNavKey extends InheritedWidget {
final Widget child;
final GlobalKey<NavigatorState> navigatorKey;
AppNavKey({
Key key,
#required this.child,
#required this.navigatorKey,
}) : super(key: key, child: child);
static GlobalKey<NavigatorState> of(BuildContext context) {
final ctx = context.dependOnInheritedWidgetOfExactType<AppNavKey>();
if (ctx == null) throw Exception('Could not find ancestor of type AppNavProvider');
return ctx.navigatorKey;
}
#override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}
extensions.dart
import 'package:flutter/widgets.dart';
import 'package:myapp/app_navkey.dart';
extension SwitchTabContext on BuildContext {
/// Get app level NavigatorState key.
/// ```dart
/// context.navigationKey();
/// ```
GlobalKey<NavigatorState> navigationKey() => AppNavKey.of(this);
}

Related

How to Passing Data from Navigator Pop to Previous Page Where The Data is Used in The Widget Inside the ListView.builder

As stated in the title. How to return data to the previous page where the data is used to list widgets.
I have read this article Flutter Back button with return data or other similar articles. The code works perfectly. But there is a problem if I want to use the data returned to the widget that is in the list.\
Note that I only want to update one ListWidget, I don't want to refresh the state of the entire HomePage like the solution in this article Flutter: Refresh on Navigator pop or go back.
Here is a simple code sample to represent the problem I'm facing.
(check on ListWidget Class and SecondPage Class below)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
HomePage class
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: ListView.builder(
itemCount: 4,
itemBuilder: (_, index){
return ListWidget(number: index+1);
},
)
),
);
}
}
ListWidget Class
class ListWidget extends StatelessWidget{
ListWidget({#required this.number});
final int? number;
String? statusOpen;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
);
},
child: Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(20),
color: Colors.amber,
child: Text(statusOpen != null ? '$number $statusOpen' : '$number Unopened'),
//
// I want to change the text here to 'has Opened' when the user returns from SecondPage
//
),
);
}
}
SecondPage Class
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context, 'has Opened');
// return 'has Opened' to await statusOpen variable
},
child: Text('Go Back'),
),
),
);
}
}
is there any solution to handle this?
If you make your listWidget a stateful widget, then you can get the solution where you just need to call setState when you return to your previous screen. And in this way you will be only changing your single list element and not the full screen.
sample code:
changing this line- class ListWidget extends StatefulWidget
and adding these lines -
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
);
setState(() {
});
},
If you used the data in your listview just call setstate after Navigator.pop like below code
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
).then((value) async {
setState(() {});
});
},

Can't use context.read() from inside FloatingActionButton

I'm using Riverpod + StateNotifier as my state management solution and I want to call a method when the FloatingActionButton is pressed but I can't use context.read() to access the provider. Also from inside my AppBar I can't use it. Here is my code:
main.dart
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
homepage.dart
final homeProvider = StateNotifierProvider<HomeNotifier, HomeState>(
(ref) => getIt<HomeNotifier>());
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: HomePageAppBar(),
body: ProviderListener<HomeState>(
provider: homeProvider,
onChange: (context, state) {
state.errorMessage.fold(() {}, (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
),
);
});
},
child: Consumer(builder: (context, watch, child) {
final state = watch(homeProvider);
if (state.isLoading)
return Center(child: CircularProgressIndicator());
return ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) =>
MyCard(playlist: state.items[index]),
);
}),
),
floatingActionButton: CreateButton(),
);
}
}
create_button.dart
class CreateButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FloatingActionButton(
child: Icon(
Icons.add_rounded,
size: 36.0,
),
onPressed: () {
// here I want to use context.read(homeProvider)
},
);
}
}
However, if I don't create a separate widget for the FloatingActionButton but instead I put it just inside the Scaffold, I can use context.read.
Reading providers from context is only available when the file you're working with has riverpod imported. Double-check your imports and hopefully that's it!

Flutter Web Navigation with persistent drawer and appbar

I'm having still no success with my Flutter Web App, doing an URL based routing and keeping things like the appbar and the drawer persistent.
The goal I'm trying to achieve is, that I can only modify the content of my App. So i.e. if the user enters the url $baseurl/#/ it should navigate to the content of page one. $baseurl/#/two to the content of page two, and so on...
My main file is something like this. As you can see it's done with Fluro Router and I tried so simplify everything to see my main goal.
import 'package:flutter/material.dart';
import 'package:newnavigationtest/routes.dart';
void main() {
FluroRouter.setupRouter();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MainLayout(),
);
}
}
class MainLayout extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
Column(
children: [
Expanded(
child: Container(
color: Colors.green,
width: 100,
child: Text("Static sidebar"),
),
),
],
),
Expanded(
child: Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: FluroRouter.router.generator,
),
)
],
),
);
}
}
The FluroRouter file looks like this
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
class FluroRouter {
static Router router = Router();
static Handler _routeOneHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
Container(child: Center(child: Text("Later content for page 1"))));
static Handler _routeTwoHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
Container(child: Center(child: Text("Later content for page 2"))));
static void setupRouter() {
router.define(
'/',
handler: _routeOneHandler,
);
router.define(
'/two',
handler: _routeTwoHandler,
);
}
}
Clearly, if the user enters a URL nothing happens at the moment. I think this is because the main navigator in the MaterialApp is consuming the input from the user and not passing it through to my Navigator in the Scaffold. But I'm absolutely not sure.
Can anybody point me the right way to achieve the wanted behavior?
Thanks!
Update 1:
I also tried something like using the builder to build the child and keep the rest persistent.
(This leads me to the behavior I'm looking for, except for the OverLay Error)
import 'package:flutter/material.dart';
import 'package:newnavigationtest/routes.dart';
void main() {
FluroRouter.setupRouter();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: FluroRouter.router.generator,
builder: (context, child) {
return MainLayout(child: child);
},
);
}
}
class MainLayout extends StatelessWidget {
final Widget child;
MainLayout({#required this.child});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
Column(
children: [
Expanded(
child: Container(
color: Colors.green,
width: 100,
child:
Tooltip(message: "Hello", child: Text("Static sidebar")),
),
),
],
),
Expanded(
child: child,
)
],
),
);
}
}
But this leads to a missing Overlay at the tooltip.
════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building Tooltip("Hello", dirty, state: _TooltipState#93c0f(ticker inactive)):
No Overlay widget found.
Tooltip widgets require an Overlay widget ancestor for correct operation.
Because Fluro redirects without persistence, you may have to have each page have a Scaffold with an AppBar and Drawer, most efficient way would to have a custom scaffold you could re-use.
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
void main() {
FluroRouter.setupRouter();
runApp(
MaterialApp(
title: 'Your website',
onGenerateRoute: FluroRouter.router.generator
),
);
}
class CustomScaffold extends StatelessWidget {
final Widget body;
CustomScaffold({this.body});
#override
Widget build(BuildContext context) {
return Scaffold(
//your appbar here
appBar: AppBar(),
//your drawer here
drawer: Drawer(),
body: body,
);
}
}
class FluroRouter {
static Router router = Router();
static Handler _routeOneHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) => PageOne());
static Handler _routeTwoHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) => PageTwo());
static void setupRouter() {
router.define(
'/',
handler: _routeOneHandler,
);
router.define(
'/two',
handler: _routeTwoHandler,
);
}
}
class PageOne extends StatelessWidget {
Widget build(BuildContext context) {
return CustomScaffold(
body: Container(child: Text('Page 1')),
);
}
}
class PageTwo extends StatelessWidget {
Widget build(BuildContext context) {
return CustomScaffold(
body: Container(child: Text('Page 2')),
);
}
}

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(...),
);
}
...

Flutter : Navigator operation requested with a context that does not include a Navigator

I have a scenario wherein I check the value of SharePreferences based on the value it will redirect the user to HomePage or LandingPage. I am not sure where did I got wrong? but I am getting this error below: I guess its not getting the context right any idea how do I get it?.
Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
E/flutter (11533): 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.
Here is my code:
import 'package:credit/src/pages/landing.dart';
import 'package:flutter/material.dart';
import 'package:credit/src/pages/credit/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
_LoadingPageState createState() => _LoadingPageState();
}
class _LoadingPageState extends State<MyApp> {
#override
void initState() {
super.initState();
getUserStatus().then((userStatus) {
if (userStatus == null) {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return LandingPage();
}));
} else {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return HomePage();
}));
}
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
));
}
}
Future<String> getUserStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String userStatus = prefs.getString('userstatus');
print("==On Load Check ==");
print(userStatus);
return userStatus;
}
When you call Navigator.of(context) framework goes up in widget tree attached to provided context and tries to find the closest Navigator.
The widget tree you showed does not have one, so you need to include Navigator in the widget tree.
Easiest option is to use MaterialApp with your widget passed as home. MaterialApp is creating navigator inside itself. (CupertinoApp does it too)
Updated code from original example:
import 'package:credit/src/pages/landing.dart';
import 'package:flutter/material.dart';
import 'package:credit/src/pages/credit/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: LoadingPage(),
);
}
}
class LoadingPage extends StatefulWidget {
LoadingPage({Key key}) : super(key: key);
_LoadingPageState createState() => _LoadingPageState();
}
class _LoadingPageState extends State<LoadingPage> { // note type update
#override
void initState() {
super.initState();
getUserStatus().then((userStatus) {
if (userStatus == null) {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return LandingPage();
}));
} else {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return HomePage();
}));
}
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
));
}
}
Future<String> getUserStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String userStatus = prefs.getString('userstatus');
print("==On Load Check ==");
print(userStatus);
return userStatus;
}
I have changed my code from
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo App',
theme: ThemeData(
primarySwatch: white,
scaffoldBackgroundColor: Colors.white,
),
home: Scaffold(
appBar: AppBar(
title: Text('Demo App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
HomeScreen(title: 'Demo Home')));
},
child: Text('Open Home Screen'))
],
),
),
),
);
}
To
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo App',
theme: ThemeData(
primarySwatch: white,
scaffoldBackgroundColor: Colors.white,
),
home: InitScreen());
}
}
class InitScreen extends StatelessWidget {
const InitScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Demo App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
HomeScreen(title: 'Demo Home')));
},
child: Text('Open Home Screen'))
],
),
),
);
}
What changed?
Create a separate widget for home code in MyApp with InitScreen
What was the issue?
When we try to push Route by using Navigator.of(context), flutter will
try to find Navigator in the widget tree of the given context. In the
initial code, there was no widget that has Navigator. So, create a
separate widget for home code. And the MaterialApp widget in MyApp
will have Navigator.