Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 398
Roshan Avatar asked May 26 '19 13:05

Roshan


People also ask

Why does my widget rebuild when I use keyboard?

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.

How do you reduce widget rebuild?

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.


2 Answers

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

like image 185
creativecreatorormaybenot Avatar answered Oct 09 '22 03:10

creativecreatorormaybenot


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(),
      );
    },
  );
}
like image 25
aurangzaibsiddiqui Avatar answered Oct 09 '22 04:10

aurangzaibsiddiqui