I am under the impression that using AutomaticKeepAliveClientMixin would prevent the states dispose() callback from being called when the Widget isn't visible anymore.
However, I have a situation where dispose() and initState() get called every time I hide/show a Widget, even though I implemented AutomaticKeepAliveClientMixin correctly.
class IdleScreenState extends State<IdleScreen> with AutomaticKeepAliveClientMixin {
#override
void initState() {
super.initState();
print('IdleScreen initState');
}
#override
void dispose() {
print('IdleScreen dispose');
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
// ...build the page...
}
#override
bool get wantKeepAlive => true;
}
This is how I hide/show this Widget
class MainScreen extends State<MainScreen> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return somecondition ? IdleScreen() : OtherScreen();
}
#override
bool get wantKeepAlive => true;
}
Every time this Widget (screen) is shown, initState()gets called, and every time I hide it, dispose() gets called. It's as if the AutomaticKeepAliveClientMixin has no effect. All other similar issues I could find seem to be due to either missing the wantKeepAlive => true or the super.build(context), but they are 100% there in the code.
I tried supplying a GlobalKey for IdleScreen as well, but that didn't have any effect.
However, if I use an IndexedStack or Offstage to hide/show the widget, it works as expected (initState() and dispose() don't get called when hiding/showing the widget).
IndexedStack(
index: somecondition ? 0 : 1,
children: [
IdleScreen(),
OtherScreen()
],
),
Maybe I'm mistaken, but isn't the whole purpose of AutomaticKeepAliveClientMixin to not have to manually keep the widget around using this technique?
This is in a web project, if that matters.
The type argument T is the type of the StatefulWidget subclass of the State into which this class is being mixed.
you have to pass the widget class name like this..
class IdleScreenState extends State<IdleScreen>
with AutomaticKeepAliveClientMixin <IdleScreen> {...
Related
I have a statefull widget that is inside a bloc builder, my question is if there is anyway I can keep the state of the widget between rebuilds? Because every time the bloc builder is triggered the statefull widget state is being reseted.
Thank you!
You can use AutomaticKeepAliveClientMixin.
Use it with your state class.
You will have to override wantKeepAlive method in your state class.
return true in your case.
Example would be
class _FooWidgetState extends State<FooWidget> with AutomaticKeepAliveClientMixin {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
// your build method
}
#override
bool wantKeepAlive => true;
}
I am seeing this mounted syntax. What is it for? Could you give me sample?
TL;DR: A widget is mounted if it has state. If the widget is no longer mounted, i.e it has been closed or disposed, its state can no longer be updated. Therefore, we check if a widget is mounted to determine its state can still be updated.
Mounting is the process of creating the state of a StatefulWidget and attaching it to a BuildContext.
Take the following example:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
#override
Widget build(BuildContext context) {
return Container(
);
}
}
The widget is assigned its state (_ExampleState) when the createState() method is called.
As soon as it is assigned its state, the widget becomes mounted.
Why is that important?
When a widget is unmounted in the dispose method of a StatefulWidget, it loses its state. This happens when it is no longer in the tree. I.e, it is has been closed, or no longer exists.
#override
void unmount() {
super.unmount();
state.dispose();
assert(() {
if (state._debugLifecycleState == _StateLifecycle.defunct)
return true;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${state.runtimeType}.dispose failed to call super.dispose.'),
ErrorDescription(
'dispose() implementations must always call their superclass dispose() method, to ensure '
'that all the resources used by the widget are fully released.'
),
]);
}());
// This is the key
state._element = null;
}
This basically means the state can't be updated and setState can no longer be called. So when you check if a widget is mounted, you're checking if its state can still be updated.
Use case:
Going back to our example Stateful Widget example, let's say we had a number that we wanted to update 30 seconds after the Widget is created.
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
int count = 0;
#override
void initState() {
Future.delayed(const Duration(seconds: 30), () {
setState(() => count = 5);
});
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('count $count'),
));
}
}
Our code will work fine, as long as the widget is disposed of or closed. If it is disposed of, we will get the famous error:
setState() called after dispose()
To prevent this, all we have to do is check if our widget still has state before updating it.
#override
void initState() {
Future.delayed(const Duration(seconds: 30), () {
if (mounted) setState(() => count = 5);
});
super.initState();
}
It represents whether a state is currently in the widget tree.
https://api.flutter.dev/flutter/widgets/State/mounted.html
You shouldn't call setState() on a state that is not currently in the tree.
Edit: The other answer provides a simple example. I should also mention that the described behavior is evident from the StatefulWidget lifecycle: https://flutterbyexample.com/lesson/stateful-widget-lifecycle
It's opinionated, but as far as I can see, it's a rare ocasion when you have to check for mounted, because you unsubscribe from outside events in dispose(). Even the Future from the example could be wrapped in CancelableOperation to cancel it in dispose(), which is before mounted == false
I have this problem, i have a home page where it has tabs. I like when i switch tabs to make the TabBar show black the tab that is selected and also i want to change the color of the whole Scaffold. So i made also a custom controller and used it like this:
TabController _controller;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 5);
_controller.index = 1;
_controller.addListener(() {
if (!_controller.indexIsChanging) {
setState(() {
scaffoldColor = colors[_controller.index];
});
}
});
}
The thing is that in this way all of my tabs are going to be rebuild and this is very bad because i have heavy tasks in few of them.
I also have used AutomaticKeepAliveClientMixin in all of the tabs but it didn't fix the problem. By the way i used it like this:
class Tab1 extends StatefulWidget {
#override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
print("Tab 1 Has been built");
return Text("TAB 1");
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
If for heavy tasks you mean a Future, you should place it inside initState.
See this answer: How to load async Stream only one time in Flutter?.
i need some help understanding how to obtain data from inherited widget.
I usually get the parameter from my widget directly from the build method using
#override
Widget build(BuildContext context) {
//THIS METHOD
var data = StateContainer.of(context).data;
return Container(child:Text("${data.parameter}"));
}
But this method cant be called from initState since there is no buildContext yet.
I need in the initState method to have that parameter (i call my fetch from server in that and i need to pass that data to my function), so, how should i do it?
#override
void initState() {
otherData = fetchData(data);
super.initState();
}
I tried using didChangeDipendencies() but it is called every time the view is rebuilt (popping from screen, etc.) so it is not what i want to use and neither the FutureBuilder widget.
Any suggestion?
First, note that you probably do want to use didChangeDependencies. But you can't just do your call there without any check. You need to wrap it in an if first.
A typical didChangeDependencies implementation should look similar to:
Foo foo;
#override
void didChangeDependencies() {
super.didChangeDependencies();
final foo = Foo.of(context);
if (this.foo != foo) {
this.foo = foo;
foo.doSomething();
}
}
Using such code, doSomething will be executed only when foo changes.
Alternatively, if you are lazy and know for sure that your object will never ever change, there's another solution.
To obtain an InheritedWidget, the method typically used is:
BuildContext context;
InheritedWidget foo = context.inheritFromWidgetOfExactType(Foo);
and it is this method that cannot be called inside initState.
But there's another method that does the same thing:
BuildContext context;
InheritedWidget foo = context.ancestorInheritedElementForWidgetOfExactType(Foo)?.widget;
The twist is:
- this method can be called inside initState
- it won't handle the scenario where the value changed.
So if your value never changes, you can use that instead.
1, If you only need InheritedWidget as a Provider of parameter for Widget.
You can using on initState as bellow:
#override
void initState() {
super.initState();
var data = context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
}
2, If you need listener to re-render widget when data of InheritedWidget change. I suggest you wrapper your StatefulWidget insider a StatelessWidget,
parameter of StatefulWidget is passed from StatelessWidget, when InheritedWidget change data, it will notify to StatelessWidget, on StatefulWidget we will get change on didChangeDependencies and you can refresh data.
This is code guide:
class WrapperDemoWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
DemoData data = StateContainer.of(context).data;
return Container();
}
}
class ImplementWidget extends StatefulWidget {
DemoData data;
ImplementWidget({this.data});
#override
_ImplementWidgetState createState() => _ImplementWidgetState();
}
class _ImplementWidgetState extends State<ImplementWidget> {
#override
void initState() {
super.initState();
//TODO Do sth with widget.data
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
//TODO Do change with widget.data
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I prefer the solution with didChangeDependencies because Future.delayed solution is a bit hack, looks unprofessional and unhealthy. However, it works out of the box.
This is the solution I prefer:
class _MyAppState extends State<MyApp> {
bool isDataLoaded = false;
#override
void didChangeDependencies() {
if (!isDataLoaded) {
otherData = fetchData(data).then((_){
this.isDataLoaded = true;
});
}
super.didChangeDependencies();
}
...
You can also get the context in initState, try using a future with duration zero. You can find some examples here
void initState() {
super.initState();
Future.delayed(Duration.zero,() {
//use context here
showDialog(context: context, builder: (context) => AlertDialog(
content: Column(
children: <Widget>[
Text('#todo')
],
),
actions: <Widget>[
FlatButton(onPressed: (){
Navigator.pop(context);
}, child: Text('OK')),
],
));
});
}
i use it to make loading screens using inherited widgets and avoid some global variables
I'm trying to share same bloc across two routes.
But when I come back from second route the bloc get automatically disposed so in the first route I find myself with all the stream closed.
For example this is the first route (HomePage) where I instantiate the bloc, download a list from api and show it in the build method.
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GroupsBloc _groupBloc;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_groupBloc = GroupsBloc();
_groupBloc.getAll();
}
#override
void dispose(){
_groupBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
}
Then I navigate to a second screen where I can add an item to the list.
_onAddGroupPress(){
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => BlocProvider<GroupsBloc>(bloc: _groupBloc, child: GroupPage()),
fullscreenDialog: true
),
);
}
In the second screen I retrieve the bloc and I use it to add an item, then I go back to Home Page.
class GroupPage extends StatefulWidget {
#override
_GroupPageState createState() => _GroupPageState();
}
class _GroupPageState extends State<GroupPage> {
FormBloc _formBloc; //another bloc
GroupsBloc _groupBloc;
#override
void initState(){
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_formBloc = FormBloc();
_groupBloc = BlocProvider.of<GroupsBloc>(context); //retrieve of the bloc
}
#override
void dispose() {
_formBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
In the dispose method of the second screen I dispose only _formBloc bloc but _groupBloc gets disposed too, so when I come back in the first page I found myself with _groupBloc disposed and all it's stream closes.
I tought of passing the bloc to the second screen as a props but I don't know if it's the right way to do this.
It obviously depends on the scope of your Bloc, but there is nothing preventing you from sharing the same instance throughout your whole app.
ie. simply wrap your whole MaterialApp inside BlocProvider<GroupsBloc>(bloc: _groupBloc, child: MaterialApp( ... ))
if "groups" are not global to your app, you should probably just pass the bloc along to the second widget.