Why is Flutter disposing my widget state object in a tabbed interface? - flutter

I have a tabbed Flutter interfaces using DefaultTabController with 3 pages, each a stateful widget. I seem to be able to switch between the first two tabs just fine, but when I tab to the 3rd page the state object for the first page gets disposed. Subsequent state updates (using setState()) then fail.
I've overridden the dispose() method of the state object for the first page so that it prints a message when disposed. It's getting disposed as soon as I hit the third tab. I can't find documentation on why Flutter disposes state objects. (Plenty on lifecycle but not the reasons for progressing through the stages.)
Nothing unusual about setting up the tabs, I don't think.
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(title),
bottom: TabBar(
tabs: [
// Use these 3 icons on the tab bar.
Tab(icon: Icon(Icons.tune)),
Tab(icon: Icon(Icons.access_time)),
Tab(icon: Icon(Icons.settings)),
],
),
),
body: TabBarView(
children: [
// These are the 3 pages relating to to the tabs.
ToolPage(),
TimerPage(),
SettingsPage(),
],
The pages themselves are pretty simple. No animation. Just switches and sliders, etc. I think about the only departures I've made from the code examples I've seen are that I've made the main app widget stateful, I've extended the tab pages to support wantKeepAlive() and I override wantKeepAlive to set it to true (thought that might help), and I call setState() on the first two tabs from an external object, which Android Studio flags with a weak warning. State on the first two pages gets updated when reading from a websocket this app opens to a remote server. Upon further testing I've noticed it only happens only when I tab from the first to the third page. Going from first to second or second to third does not trigger the dispose.
I would expect the State object associated with a StatefulWidget to stay around and with wantKeepAlive = true don't understand why it's being disposed when I click to the third tab, especially because it doesn't happen when I click to the second or from the second to third.

This happens because TabBarView doesn't always show all tabs. Some may be outside of the screen boundaries.
In that case, the default behavior is that Flutter will unmount these widgets to optimize resources.
This behavior can be changed using what Flutter calls "keep alive".
The easiest way is to use AutomaticKeepAliveClientMixin mixin on a State subclass contained inside your tab as such:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> with AutomaticKeepAliveClientMixin {
#override
bool wantKeepAlive = true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
}
This tells Flutter that Foo should not be disposed when it leaves the screen.

Related

When AutomaticKeepAliveClientMixin Widget is back on screen, how to refresh it?

I have a tab page with AutomaticKeepAliveClientMixin, which works well.
What I want is, when switch back to this page, I can choose refresh this page or not.
As far as I know, wantKeepAlive seems work on when widget is displayed.
I also checked life cycle of State. Method like initState, activate. None of them worked.
So my question is, Will there be any thing triggered when AutomaticKeepAliveClientMixin Widget is back on screen?
Or if AutomaticKeepAliveClientMixin can't do that, is there any other way to keep page state, and
Navigator.pop(context, false);
you can use this on screen
when you send to new screen
final return = Navigator.of(context).push(MaterialPageRoute<bool>());
if(return){
//
}else{
//
}
Move the state up from the page (tab) to parent and initiate refresh in onTap of TabBar.
TabBar(
onTap: (index) {
// TODO
},
controller: _controller,
tabs: const <Widget>[
...
],
),
For a more elegant solution take a look at Simple app state management.

flutter hot reload other pages

I am beginner of flutter.
Text (printA ())
and
PrintA () {print ("A"); retuen "A";}
are executed on page A of Flutter, and "A" is displayed in the console after hot reloading. Then navigate to page B and print "B" to the console as well. At this time, if I hot reload, not only "B" but also "A" will be displayed on the console. If I repeat the screen navigation, the number of A and B displayed on the console will continue to increase with one hot reload. What is the reason for this?
class PageA extends StatelessWidget {
const WordPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: MyDrawer(),
appBar: AppBar(
title: Text("PageA"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(printA()),
],
),
),
);
}
}
printA() {
print("A");
return "A";
}
Also, if I just make a screen navigate in the emulator without hot reloading, only "A" on page A and only "B" on page B will be displayed on the console as usual. and I use m1 mac.
A widget that manages a set of child widgets with a stack discipline.
Many apps have a navigator near the top of their widget hierarchy in
order to display their logical history using an Overlay with the most
recently visited pages visually on top of the older pages. Using this
pattern lets the navigator visually transition from one page to
another by moving the widgets around in the overlay. Similarly, the
navigator can be used to show a dialog by positioning the dialog
widget above the current page.
This is from the doc https://api.flutter.dev/flutter/widgets/Navigator-class.html your new page 'B' is just above the previous page 'A' which was never destroyed. And hot reload is causing the build method of page 'A' run again and you get 'A' printed.

How do I keep my widget state when reordering them via Draggable&DragTarget?

I'm using this reorderables package. This package works by having a list of children widgets that are each wrapped with a Draggable and put inside a DragTarget. Before that the childs key is assigned to a GlobalObjectKey.
After the dragTarget is created, it is assigned(or rebuild?) to a KeyedSubTree:
dragTarget = KeyedSubtree(key: keyIndexGlobalKey, child: dragTarget);
According to the comments in the package source code, this should preserve the child widgets state (toWrap) when being dragged:
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key);
The reordering itself happens not with the DragTarget accepting the Draggable dragged into it, but rather by using the DragTarget around each child to get index of the current position the Draggable is hovering over. When the Draggable is let go, a reorder function will get called, which removes the widget (that was being dragged) from the list and inserting it into the new position.
Now comes my problem: The state of the widget is not being preserved. I made a simple TestWidget to test this:
class TestWidget extends StatefulWidget{
#override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
Color boxColor;
#override
void initState() {
super.initState();
boxColor= Colors.blue;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(color: boxColor),
child: Text("Test"),
),
FlatButton(
onPressed: (){
setState(() {
boxColor = Colors.red;
});
},
padding: EdgeInsets.all(8.0),
child: Text("Change to Red"),
color: Colors.grey,
)
],
);
}
}
This widget has a Container with a initial blue background (boxColor) and a button. When the button is pressed, it will change the boxColor to red. The moment the dragging on the widget is initiated, it is rebuild and defaults to the initial state (at least the Draggable feedback is). After the reordering that doesn't change and the widget is still in it's default state.
My plan here is to have a list of different custom widgets, where the User can modify their content and if they are not happy with the order, they can drag those widgets around and rearrange them.
My question is: How do I preserve the state of my widgets?
I'm thinking of creating a class for each widget with all state relevant variables and use that to build my widgets but that seems very bloated and not really in the mind of flutter. Isn't that supposed to be the role of the state of the StatefulWidget?
EDIT:
So I solved my problem by creating an additional class for my widget state with ChangeNotifier and then moving all my variables that I want to keep track of into this class. So I basically now have two lists, one for my widgets in the reorderable list and one for their states. I still think that this is kinda scuffed. If a widget in my list has additional children of its own, I would need to create separate state classes for each of them that need it and save them somewhere. This can get very messy, very quickly.

Flutter - Keep page static throughout lifecycle of app?

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)

Why does the Software Keyboard cause Widget Rebuilds on Open/Close?

I have a screen, which contains a Form with a StreamBuilder. when I load initial data from StreamBuilder, TextFormField show data as expected.
When I tap inside the TextFormField, the software keyboard shows up, which causes the widgets to rebuild. The same happens again when the keyboard goes down again.
Unfortunately, the StreamBuilder is subscribed again and the text box values is replaced with the initial value.
Here is my code:
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _bloc.inputObservable(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return TextFormField(
// ...
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
How do I solve this?
Keyboard causing rebuilds
It makes total sense and is expected that the software keyboard opening causes rebuilds. Behind the scenes, the MediaQuery is updated with view insets. These MediaQueryData.viewInsets make sure that your UI knows about the keyboard obscuring it. Abstractly, the keyboard obscuring a screen causes a change to the window and most of the time to your UI, which requires changes to the UI - a rebuild.
I can make the confident guess that you are using a Scaffold in your Flutter application. Like many other framework widgets, the Scaffold widgets depends (see InheritedWidget) on the MediaQuery (that gets its data from the Window containing your app) using MediaQuery.of(context).
See MediaQueryData for more information.
It all boils down to the Scaffold having a dependency on the view insets. This allows it to resize when these view insets change. Basically, when the keyboard is opened, the view insets update, which allows the scaffold to shrink at the bottom, removing the obscured space.
Long story short, the scaffold adapting to the adjusted view insets requires the scaffold UI to rebuild. And since your widgets are necessarily children of the scaffold (likely the body), your widgets are also rebuilt when that happens.
You can disable the view insets resizing behavior using Scaffold.resizeToAvoidBottomInset. However, this will not necessarily stop the rebuilds as there might still be a dependency on the MediaQuery. I will explain how you should really think about the problem in the following.
Idempotent build methods
You should always build your Flutter widgets in a way where your build methods are idempotent.
The paradigm is that a build call could happen at any point in time, up to 60 times per second (or more if on a higher refresh rate).
What I mean by idempotent build calls is that when nothing about your widget configuration (in the case of StatelessWidgets) or nothing about your state (in the case of StatefulWidgets) changes, the resulting widget tree should be strictly the same. Thus, you do not want to handle any state in build - its only responsibility should be representing the current configuration or state.
The software keyboard opening causing rebuilds is simply a good example for why this is so. Other examples are rotating the device, resizing on web, but it can really be anything as your widget tree starts to get complex (more on that below).
StreamBuilder resubscribing on rebuild
To come back to the original question: in this case, your problem is that you are approaching the StreamBuilder incorrectly. You should not feed it a stream that is recreated each build.
The way stream builders work is by subscribing to the initial stream and then resubscribing whenever the stream is updated. This means that when the stream property of the StreamBuilder widget is different between two build calls, the stream builder will unsubscribe from the first and subscribe to the second (new) stream.
You can see this in the _StreamBuilderBaseState.didUpdateWidget implementation:
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
The obvious solution here is that you will want to supply the same stream between different build calls when you do not want to resubscribe. This goes back to idempotent build calls!
A StreamController for example will always return the same stream, which means that it is safe to use stream: streamController.stream in your StreamBuilder. Basically, all controller, behavior subject, etc. implementations should behave this way - as long as you are not recreating your stream, StreamBuilder will properly take care of it!
The faulty function in your case is therefore _bloc.inputObservable(), which creates a new stream each time instead of returning the same one.
Notes
Note that I said that build calls can happen "at any point in time". In reality, you can (technically) control exactly when every build happens in your app. However, a normal app will be so complex that you cannot possibly have control over that, hence, you will want to have idempotent build calls.
The keyboard causing rebuilds is a good example for this.
If you think about it on a high level, this is exactly what you want - the framework and its widget (or widgets that you create) take care of responding to outside changes and rebuilding whenever necessary. Your leaf widgets in the tree should not care about whether a rebuild happens - they should be fine being placed in any environment and the framework takes care of reacting to changes to that environment by rebuilding correspondently.
I hope that I was able to clear this up for you :)
I faced a similar issue in my application. What resolved my issue was to make my "widget tree clean" as suggested by one of the programmers on this forum.
Try moving the definition of your stream to init state. This will prevent your stream from disconnecting and reconnecting every time there is a rebuild.
var datastream;
#override
void initState() {
dataStream = _bloc.inputObservable();
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: dataStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return TextFormField(
// ...
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
It can be resolved by creating a stateful widget like following
class StatefulWrapper extends StatefulWidget {
final Function onInit;
final Widget child;
const StatefulWrapper({#required this.onInit, #required this.child});
#override
_StatefulWrapperState createState() => _StatefulWrapperState();
}
class _StatefulWrapperState extends State<StatefulWrapper> {
#override
void initState() {
if (widget.onInit.call != null) {
widget.onInit();
}
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
and wrapping the stateless widget using the wrapper
Widget body;
class WidgetStateless extends StatelessWidget {
WidgetStateless();
#override
Widget build(BuildContext context) {
return StatefulWrapper(
onInit: () async {
//Create the body widget in the onInit
body = Container();
},
child : body
)
}