'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?
Solution 1:[1]
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 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 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 :)
Solution 2:[2]
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
)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | creativecreatorormaybenot |
Solution 2 | Milinda Sandaruwan |