I was hoping to use InheritedWidget at the root level of my Flutter application to ensure that an authenticated user's details are available to all child widgets. Essentially making the Scaffold the child of the IW like this:
#override
Widget build(BuildContext context) {
return new AuthenticatedWidget(
user: _user,
child: new Scaffold(
appBar: new AppBar(
title: 'My App',
),
body: new MyHome(),
drawer: new MyDrawer(),
));
}
This works as expected on app start so on the surface it seems that I have implemented the InheritedWidget pattern correctly in my AuthenticatedWidget, but when I return back to the home page (MyHome) from elsewhere like this:
Navigator.popAndPushNamed(context, '/home');
This call-in the build method of MyHome (which worked previously) then results in authWidget being null:
final authWidget = AuthenticatedWidget.of(context);
Entirely possible I'm missing some nuances of how to properly implement an IW but again, it does work initially and I also see others raising the same question (i.e. here under the 'Inherited Widgets' heading).
Is it therefore not possible to use a Scaffold or a MaterialApp as the child of an InheritedWidget? Or is this maybe a bug to be raised? Thanks in advance!
MyInherited.of(context) will basically look into the parent of the current context to see if there's a MyInherited instantiated.
The problem is : Your inherited widget is instantiated within the current context.
=> No MyInherited as parent
=> crash
The trick is to use a different context.
There are many solutions there. You could instantiate MyInherited in another widget, so that the context of your build method will have a MyInherited as parent.
Or you could potentially use a Builder to introduce a fake widget that will pass you it's context.
Example of builder :
return new MyInheritedWidget(
child: new Builder(
builder: (context) => new Scaffold(),
),
);
Another problem, for the same reasons, is that if you insert an inheritedWidget inside a route, it will not be available outside of this route.
The solution is simple here !
Put your MyInheritedWidget above MaterialApp.
above material :
new MyInherited(
child: new MaterialApp(
// ...
),
)
Is it therefore not possible to use a Scaffold or a MaterialApp as the
child of an InheritedWidget?
It is very possible to do this. I was struggling with this earlier and posted some details and sample code here.
You might want to make your App-level InheritedWidget the parent of the MaterialApp rather than the Scaffold widget.
I think this has more to do with how you are setting up your MaterialWidget, but I can't quite tell from the code snippets you have provided.
If you can add some more context, I will see if I can provide more.
Related
I'm trying to implement dark mode in my flutter app using provider, but I'm having some issues:
I have a home screen that it has a scaffold widget. The body of the scaffold is a Stack with two different classes, as you can see here:
#override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: Constant.menuOn
? BorderRadius.circular(60.0)
: BorderRadius.circular(0.0),
child: Scaffold(
body: Stack(
children: const [
Menu(),
HomeScreen(),
],
),
),
);
}
The home screen class has another Scaffold widget, where hos body has another class called body.
It is from the body where I'm able to change the theme of my app using a switch where I can set state my BODY class with the provider as you can see here:
lsetState(() {
final provider = Provider.of<ThemeProvider>(
context,
listen: false);
rovider.toggleTheme(Constant.isDarkMode);
});
My body class change the theme however the class that is under the body class (Remember that my class "MENU" is under the class BODY because the first stake), does not change theme until restart my app (I'm using the same parameters of the body in order to know when has to change the class)...
I want to know how I can set the theme state of my menu class, or in general how I can change any other state from any class.
Note: Both of my class are StatefulWidget.
In general, setState is a very local intra-widget operation. Local data has changed, so the local view needs updating. Wanting to call the setState of another widget is a bad code smell. If you need other views depending on data you've updated, consider one of the state management solutions. (I would, for example, use a Riverpod Provider to hold the data, then watch that in every dependent view.)
I have my theme class that I hang in the widget tree using Provider:
Scaffold(
body: Provider<AppControlsThemeData>(
create: (_) => ...,
child: ...
),
);
Then somewhere below I read it with
final themeData = Provider.of<AppControlsThemeData>(context);
It works alright until I try to use this lookup in ReorderableListView. When I drag the widget that does the lookup, it fails to find the provider:
How do I hang some object in the widget tree so that its lookup works while dragging? I hang it as far as it is reasonable -- right under the Scaffold. Looks like this draggable widget gets detached from my screen's tree and I cannot control what is above it in its new tree.
I am currently learning app development with Flutter and have started learning about the Provider package. I was having some difficulty and was getting the error:
"Could not find the correct Provider above this ... Widget"
I ended up moving the Provider widget to wrap around my MaterialApp widget instead of my Scaffold Widget, and that seemed to fix things.
That being said, I'm not sure why this fixed things. Are we supposed to put our Provider widget around our MaterialApp? If so, can someone please explain why this is needed? If not, can someone explain how to determine where to place the Provider widget in our tree?
Usually, the best place is where you moved it, in the MaterialApp. This is because since that is where the app starts, the node tree will have access to the provider everywhere.
If your page is a Stateful widget - inside Widget wrap State with Provider, so you can use it inside of State. This is a much cleaner solution because you won't have to wrap your entire application.
If you need the functionality of Provider everywhere in the app - yes, wrapping the entire app is completely fine, though I'll prefer to use some kind of service for this
You could add it to any route and pass it to the route you need to use or you can add it to MaterialApp
so you can use it anywhere.
The best practice of using provider:
Place the Provider widget at the top of the widget tree. Bellow I put a template code that can be used for one more providers at the same place, by using MultiProvider widget under Provider package.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ProviderName<ModelName>(create: (_) => ModelName()),
AnotherProviderName<AnotherModelName>(create: (_) => AnotherModelName()),
],
child: MaterialApp(
title: 'App title',
theme: ThemeData(
primarySwatch: Colors.blue,
primaryColor: const Color(0xFF2196f3),
accentColor: const Color(0xFF2196f3),
canvasColor: const Color(0xFFfafafa),
),
home: MyHomePage(), // Your widget starting
),
);
}
}
For more informatin: https://pub.dev/documentation/provider/latest/
I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)
I'm reading the docs: https://docs.flutter.io/flutter/widgets/BuildContext-class.html
This can lead to some tricky cases. For example, Theme.of(context) looks for the nearest enclosing Theme of the given build context. ...
Does enclosing includes the Theme of current context? I don't understand what the tricky cases the author indicated.
The tricky situation they mentioned becomes more clear if you understand the previous statement:
In particular, this means that within a build method, the build context of the widget of the build method is not the same as the build context of the widgets returned by that build method.
Ok, not a very helpful language. Imagine:
FooWidgetA:
attributes: context, height, width
methods: build
FooWidgetB:
attributes: context, theme
methods: build
FooWidgetB gets built inside FooWidgetA build method. If you try to find theme using the context of FooWidgetA it won't find because FooWidgetA is one level above in the widget tree.
So, exemplifying their tricky situation, it looks like this:
class Foo extends StatelessWidget {
#override
Widget build(BuildContext buildMethodContext) {
return MaterialApp(
// Here we create the [ThemeData] that our method below will try to find
theme: ThemeData(primaryColor: Colors.orange),
builder: (BuildContext materialAppContext, _) {
return RaisedButton(
child: const Text('Get ThemeData'),
onPressed: () {
getThemeData(buildMethodContext); // unsucessful
getThemeData(materialAppContext); // sucessful
},
);
},
);
}
ThemeData getThemeData(BuildContext context) => Theme.of(context);
}
It's tricky because, since the two contexts are way to close, it's easy to forget that buildMethodContext is actually from the parent (Foo), so it cannot see materialAppContext.