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

2,772

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 :)

Share:
2,772
Roshan
Author by

Roshan

Still Studing

Updated on December 11, 2022

Comments

  • Roshan
    Roshan 12 months

    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?