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?
the only reason why your widgets got rebuilds after keyboard pop up. is that one or more of your widgets size depends on MediaQuery. you can try to ge your screen size from LayoutBuilder as an alternative for MediaQuery.
When the instance of a widget stays the same; Flutter purposefully won't rebuild children. It implies that you can cache parts of your widget tree to prevent unnecessary rebuilds. Thanks to that const keyword, the instance of DecoratedBox will stay the same even if build were called hundreds of times.
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.
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 StatelessWidget
s) or nothing about your state (in the case of StatefulWidget
s) 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 rebuildTo 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.
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(),
);
},
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With