I have a class which needs access to MaterialApp context, but it also needs global access from all routes.
To create a global provider, I can wrap MaterialApp with Provider, but such provider has no access to the context. Therefore, I have to provide the class after MaterialApp.
I realize I could wrap every single route with the provider because it is stateless, but I would like to know if there is a better way of doing this.
Thanks in advance!
You can use builder or onGenerateRoute of MaterialApp
MaterialApp(
builder: (context, child) {
return Provider.value(
value: // TO-DO use context
child: child,
);
}
)
You can wrap the material app in a Builder to gain acess do a brand new context.
Related
In my flutter app I have a list of instances, when user clicks one I open the instance page and provide the InstanceData model to it using
Navigator.push(context, MaterialPageRoute(builder: (context) =>
ChangeNotifierProvider(
create: (_) => InstanceData(...),
child: Instance()
)
));
This does work, but when the Instance widget does Navigator.push, the provider data is lost because my provider is below the app's Navigator.
I googled for a solution and found that I can do:
Navigator.push(context, MaterialPageRoute(builder: (context) =>
ChangeNotifierProvider(
create: (_) => InstanceData(),
child: Navigator(
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) => Instance()
);
},
)
)
));
Here's the dartpad to show it: https://dartpad.dev/?id=51fcbabb1ad401afb193558ccde3b5e1
Now, all (even deeply nested) widgets that are opened from Instance() will have access to the InstanceData. Also, I actually need multiple nesting. E.g. we click Instance and create Provider for it, then inside Instance we click Area and I want all widgets from Area() and below to have access to both InstanceData and AreaData, so I will need to create another Navigator to hold AreaData. Think of it as in this example:
https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf
which shows that we keep separate history inside each Tab. Now, if each my Instance is in its own Tab, and Instance also has tabs panel with each tab being Area that also has to keep its own history - that's what I need.
However I am new to flutter and I am not sure if that's the correct approach. Creating own Navigator on each route push just so that my Inherited/Provider data is accessible? Looks like a hack to me.
E.g. what if I want to show two Instance() widgets side by side - I would wrap them with their own Navigator just to pass the data but isn't navigator supposed to track pages/screens instead? How will it behave with the Back button?
So can somehow confirm the solution and if not, them suggest a state management for flutter that will allow to create do something like
Navigator.push(context, MaterialPageRoute(builder: (context) =>
CoolProvider(
create: (_) => InstanceData(...),
child: Instance()
)
));
where CoolProvider does work for any Navigator.push() inside any child widget of the Instance(), even if that's some "CoolNavigator.push" instead. Maybe it's some kind of sub-routes? E.g. Navigator.pushSubRoute() so that the main route is still there?
Maybe it's not even about Navigator? E.g. in the side-by-side Instances having Navigator is almost meaningless - user can't navigate to both instances at once, but I do need each Instance() to be clickable and to go to nested Areas with the InstanceData() available to those widgets.
Update: In fact I tried it side by side here:
https://dartpad.dev/?id=3c299f3972ac18a1b70b841380eaffef
and it seems to be working fine, because without the Navigator the push just replaces the whole page, while with navigator it correctly replaces just the half-side widget.
So is it the way I suppose to do? Any drawbacks - need to use navigatorKey, etc?
P.S. There are of course alternatives like passing InstanceData to each child ctor but that's a lot of manual properties passing around... having the Provider is surely a better way.
I'm working on a simple demo where the user logs in, sees a list of items, clicks on an item, and then sees its details. These are 3 screens but let's say two flows where one deals with login and the other with items.
I'm managing my state using provider package. I placed a LoginRepository provider on my root widget because that state is needed everywhere. This works great.
Now I need an ItemRepository which should be available on the ItemListScreen and the ItemDetailScreen. To implement that scoping, I decided to create a common ancestor widget called ItemScreens. This is where I placed my provider and my nested Navigator.
I navigate my screens using Navigator.of(context) to make sure the closes ancestor navigator is used. I fetch my provider using Provider.of<ItemRepository>(context) to make sure the closes ancestor state is used.
This apparently does not work because I get a Could not find the correct provider. I understand that the provider from the sibling route cannot be accessed by another sibling but here I'm nesting navigators I would expect the ItemRepository to be placed at ItemScreens which then handles subrouting so that these two routes can fetch providers from the parent route?
Sidenote:
Flutter documentation clearly states that providers should be lifted to achieve proper scoping. But I've never seen an example where the provider was lifted to a non-root widget. If all we can do is place providers into the root widget, that should be clearly stated.
EDIT: Re-creating providers on several widgets is kind of an alternative but that's not really scoping it's just pseudo passing by value.
EDIT: Putting all providers on the root widget with MultiProvider isn't a good idea because it creates a global state anti-pattern. Some states should also simply not be created unless needed i.e. the user is visiting that flow/screen.
You can wrap your MaterialApp Widget in main.dart with Multiprovider and give the list of providers you are using, That way App knows which class to treat as providers.
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => FirstProvider(),
),
ChangeNotifierProvider(
create: (_) => SecondProvider(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Auro',
home: _decideMainPage(context),
// routes: routes,
theme: AllCoustomTheme.getThemeData(),
),
)
I would like to use both device_preview and auto_route. To do so, I need to add the respective codes to the builder property of MaterialApp. However, since there is only one builder property in MaterialApp widget, I can only use one of the packages. What can I do to use both?
The builder property in Material App will give you context and a Native navigator in case you want to wrap it with some theme data or something, so instead of passing the native navigator pass your ExtendedNavigator.
MaterialApp(
builder: (context, nativeNavigator) => DevicePreview.appBuilder(
context,
ExtendedNavigator(router: Router()),
),
If anyone is using auto_route in version 0.6.2 or above, you might be struggling with the solution, as the answer above doesn't work anymore, since auto_route is using ExtendedNavigator.builder now. This is how you can solve the issue:
MaterialApp(
locale: DevicePreview.of(context).locale,
builder: (context, nativeNavigator) => DevicePreview.appBuilder(
context,
ExtendedNavigator.builder<Router>(
router: Router(),
)(context, nativeNavigator),
),
);
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()));
My app makes multiple requests to the server. Sometimes the server may ask the user to re-login, similar to this question:
Flutter: how to force an application restart (in production mode)?
I could do something like this,
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
(Route<dynamic> route) => false);
but I need to have a BuildContext for this.
Is there a way to get a current (most recently used) context while in a non-Widget class? I know I can pass the context as an argument every time I'm making a server call, but I hope to find a less intrusive way to do this.
The reason you are looking for context is because you'd like to get hold of the Navigator. (All the Navigator.pushXXX calls do a Navigator.of(context) under the hood to find the (typically) one and only navigator up near the top of the widget tree.)
If what you really want is just access to the navigator, there's another way to do this. Make your app stateful and put a global key in its state.
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
In the app's build (or top inherited widget's build), when you create the material app, pass it the key.
#override
Widget build(BuildContext context) {
return _SomeInherited(
data: this,
child: MaterialApp(
navigatorKey: navigatorKey,
title: 'Some Title',
theme: someTheme,
home: FrontPage(),
),
);
}
Now, from the level of the app, you can use navigatorKey.currentState.pushXXX