Calling Navigator.of anywhere without context? - flutter

I've been setting up a tabbed navigation app based on this wonderful tutorial (https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf).
Now I would like to display a modal overlay login route that covers the whole screen. My login controller checks if the user is logged in and I would like to fire an event on which the modal login route appears. The problem I have is now, that I don't have a context object where I receive the signal to display the login route:
Navigator.of(context).pushReplacementNamed('/');
How can I solve this or is this the wrong approach?
My User controller is a singleton object that gets initiated at app start. It checks then the user data model and if that is not set, it wants to invoke the login screen / route.
Thanks for any pointer in the right direction.
Martin

Define a navigator key that accesses from everywhere in-app (e.g in main class global space ) and pass it to root MaterialApp navigator key property in the build method
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
then :
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
//...
);
}
then you can access context everywhere with doing like this:
navigatorKey.currentContext
example of navigating:
Navigator.of(navigatorKey.currentContext).pushReplacement(
MaterialPageRoute(builder: (_) => SecondScreen()));

You could use a globalKey to access the context of the widget u like (you have to pass it a key in the constructor) , and leave it in the global space, or static in any class....
although not the most elegant approach, should work

you can use this package to skip the required context.
https://pub.dev/packages/one_context
// go to second page using named route
OneContext().pushNamed('/second');
// go to second page using MaterialPageRoute
OneContext().push(MaterialPageRoute(builder: (_) => SecondPage()));

Related

Flutter Webview with GoRouter (navigation 2.0) - how to handle back button press

According to this blog, if using Navigator 2.0 and/or (in my case) GoRouter you can no longer override the phone's back button using the "WillPopScope" and onWillPop function call. Navigator 2.0 now uses PopRoute to go back.
This causes an issue when using webview on a flutter page. If the user navigates to another web page within that webview and then clicks the back button on their phone they naturally expect the webview navigate back to the previous web page. But instead it takes the user off that page and back to their previous flutter page.
Is there any way around this? Can I have my back button first check whether there is a controller.canGoBack() like I used to be able to do with the old Navigator system?
I have found a solution. Convoluted, but functional:
I had to create a custom "backButtonDispatcher" and add it to the main.dart MaterialApp.router function
child: Builder(builder: (BuildContext context) {
final router = Provider.of<MainRouter>(context, listen: false).router;
backbuttondispatcher = backButtonDispatcher(router.routerDelegate, settings);
return MaterialApp.router(
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
routerDelegate: router.routerDelegate,
backButtonDispatcher: backbuttondispatcher,
.
.
.
I created the new dispatcher in the router folder and called it "backbuttondispatcher.dart.
import 'package:flutter/material.dart';
class backButtonDispatcher extends RootBackButtonDispatcher {
final RouterDelegate _routerDelegate;
final _settings;
backButtonDispatcher(this._routerDelegate,this._settings)
: super();
Future<bool> didPopRoute() async {
//Can user leave the page?
if (!_settings.canLeavePage) {
//no, as the webview widget has flagged canLeavePage as false
_settings.goBackToPreviousWebsite();
return true;
}else{
//yes, perform standard popRoute call
return _routerDelegate.popRoute();
}
}
}
Using a shared class reference (I used "_settings") I store a flag that says whether or not the user has traversed through more than one web page - if TRUE, the back button dispatcher won't go back to a previous route/page and instead call another function (pointer) that handles going back to a previous web page in the webview widget route. But if FALSE, the dispatcher performs it's standard didPopRoute function.
Additionally, on all other routes/pages with a webview, the pointer function and boolean need to reset to null and false. This is not ideal but fortunately there aren't that many pages in the app.
It annoys me that they changed the back button functionality for main route/page navigation but didn't take in to consideration the fact that the back button can also be used for going back to a previous webpage. I understand that we shouldn't really be showing web pages with apps anyway but we lowly developers don't always have the power to deny app requirements from higher up.

Go Router (Flutter)- push the exact same route and refresh that route

With GoRouter, Is there a way to push the exact same route with a different parameter and make the page reload? I am thinking of something like Navigator.pushReplacement
I am using context.go('/my_page/?param={someID}') to push to MyPage (a stateful widget)
The initial push to this route works fine and I load up the page for the particular ID
I am trying to push this same route again (replace the route and reload with different ID) using context.go('/my_page/?param={differentID}'). My breakpoints are hitting the return statement in my GoRoute pageBuilder, and I also hit a breakpoint in MyPage.build. I see the new ID passed into this widget when breakpointing in the build.
But the Page does not rebuild visually (or break in initState - side note, init state is used to load up a couple cubits with the passed in ID - could the page being a stateful widget be the problem?)
Maintain state on the Material Page is false. Also, pushing different routes works just fine.
Is this a stateful widget issue (meaning relocate all of my cubit init calls)? or is there a different way to push the same route?
EDIT _____________
This question was specific to rebuilding the same route, but the greater problem I was working on was to infinitely drill into the same page over and over again, while maintaining the navigation stack.
I was using a state manager to hold a list of routes and trigger the navigation by using context.go() to replace the current route completely.
is there a way to dynamically nest routes with context.push() while maintaining the entire nav stack?
After some research, I have noticed that you can redirect to the same page by using pageKey: UniqueKey() in the definition of the GoRoute().
Example code from my project:
GoRoute(
path: RoutePaths.serviceDetail.value,
name: RouteNames.serviceDetail.value,
pageBuilder: (context, state) {
return CustomTransitionPage<void>(
key: UniqueKey(),
child: const ServiceDetailPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
PageTransitions.subpageTransition(animation, child),
);
},
),
I think context.replace would accomplish what you are trying to do, since it should replace what is currently on the stack.

Flutter show SnackBar without context

I have a flutter app that makes many different http requests in different screens of the app. Now, I want to implement some general error handling, for example, whenever there is no connectivity, I want to show a SnackBar, no matter where I currently am in the app. I managed to write a method that gets called everytime there is an exception in one of my requests. Now, I want to show the SnackBar from within this method. But I don't have access to the BuildContext in this method, and I can't pass it as a parameter to the method either.
So, is there a way to show a SnackBar without having the context? For example, a method to get the context of the currently active screen, and then using it to show the SnackBar?
Suggesting to use a package as pointed by Kaival is not the right way to go, since you won't learn the right solution for your problem other than relying on 3rd party packages and also over-engineering your app with those when sometimes not needed.
Tu use a SnackBar without a context it's actually very simple through a key. You can create a GlobalKey<ScaffoldState> wherever you want and assign it to the Scaffold where you want to display the SnackBar.
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget build(BuildContext context) {
return Scaffold(key: _scaffoldKey);
}
and then anywhere you want, you can check its state to launch a SnackBar as such:
void foo() {
if(_scaffoldKey.currentState != null) {
_scaffoldKey.currentState.showSnackBar();
}
}
You just need to make sure the state is mounted (this is, the Scaffold you want is available, before displaying it.
Just make sure that you are using the new ScaffoldMessenger API you may want to consider adding it to your root MaterialApp a ScaffoldMessengerState so you can use it everywhere.
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
MaterialApp(
scaffoldMessengerKey: rootScaffoldMessengerKey,
home: ...
)
and call it accordingly
rootScaffoldMessengerKey.currentState.showSnackBar();
Refer to the docs for more info.
Hi you could use a globalkey as a navigation key associated with your MaterialApp. That would allow you to also navigate eventually to an error scree or anywhere else.
With the globalkey, you will be able to retrieve the context everywhere (at least when your app is in foreground and there actually is a context).
Although it is not always really reccomended to have a global variable, you can do Something like this:
In your main create a GlobalVariable class containing the key:
class GlobalVariable {
/// This global key is used in material app for navigation through firebase notifications.
static final GlobalKey<NavigatorState> navigatorState = GlobalKey<NavigatorState>();
}
Than declare the key as navigation key in your MaterialApp:
MaterialApp(
navigatorKey: GlobalVariable.navigatorState,
Finally access the key and context everywhere:
GlobalVariable.navigatorState.currentContext

Flutter detect when Navigator is ready

Working on a Flutter app, I am handling deeplinks with a SDK for which I am given a listener that I must handle as soon as the app starts, therefore in the main().
Once a deeplink is received, I must navigate to the proper screen, based on the parameters passed along the deeplink data.
Since I receive the deeplink in the main function, I am detached from the Context of the app, therefore to access the NavigatorState I used a navigation singleton with a GlobalKey passed to my CupertinoApp's navigatorKey. I later use this key to retrieve the NavigatorState and call push. (instead of Navigator.of(context)....
However, if opening the app from a deeplink, it is very likely that the navigatorKey does not contain anything (yet).
How can I detect/wait until the Navigator is ready ?
As of right now my approach is to add a WidgetsBinding.instance.addPostFrameCallback in my App's initState, that resolves a promise to indicate when the GlobalKey pointing to the NavigatorState can be used... I'm sure there is a beter way to achieve this.
For this i make the initialRoute as a Loading screen and execute all my logic inside it , that way i can have different initialRoutes and i don't have to worry about not having MediaQuery, or Navigator, or even ThemeData.
However the question specifies a way to know if the navigator is available, for this i would use the builder function of MaterialApp/WidgetsApp/CupertinoApp, it's used to override the navigator altought i use it to add pading and a global background color and even override the navigator widget with a custom one,
CupertinoApp(
...
builder: (context, child){
//The navigator is ready
Future.delayed(Duration.zero,
() => print(navigatorKey.currentContext)); //We use a delayed since the child needs to be returned first. It's kinda like a hack 7u7
return child;
}

InheritedWidget not accessible from new Route

I'm building a basic Flutter app with Bloc pattern. This is my structure so far.
MainProvider contains the Repository, which I can pass in every Bloc of every sub page (one Bloc per page, basically).
The problem is that, if I want to access MainProvider like this:
final provider = MainProvider.of(context);
from MenuPage, or OtherPage, which I access to by navigating from HomePage with
Navigator.push(context,
MaterialRoutePage(
(context) => MenuPage(homePageParam); //or OtherPage(homePageParam)
);
the .of method returns null.
How can I properly access that InheritedWidget? Should I do another type of Navigator.push?