Flutter Keyboard显示/关闭导致窗口小部件重建?

时间:2019-05-26 13:09:35

标签: flutter keyboard textfield bloc stream-builder

当我从Form加载初始数据时,我有一个包含StreamBuilderStreamBuilder的屏幕,TextFormField显示了预期的数据。 当我点击TextFormField键盘时,这将导致小部件重建, 然后再一次,在编辑之后,键盘会下降,这会导致再次重新构建小部件,并且在重新构建小部件StreamBuilder时会再次订阅,并将文本框值替换为初始值。

这是我的代码

  Widget _formUI() {

    return StreamBuilder<ConcreteEstimationForm>(
        stream: _bloc.inputObservable(),
        builder: (context, AsyncSnapshot<ConcreteEstimationForm> snapshot) {
          if (snapshot.hasData) {
            ConcreteEstimationForm form = snapshot.data;
            _descriptionController.text = form.description;
            return Column(
              children: <Widget>[
                TextFormField(
                  decoration: const InputDecoration(labelText: "Description"),
                  keyboardType: TextInputType.text,
                  validator: _descriptionValidator,
                  controller: _descriptionController,
                  onSaved: (String value) {
                    _description = value;
                  },
                ),
                ],
            );
          } else {
            return new Center(
              child: new CircularProgressIndicator(),
            );
          }
        });
  }

有人遇到这个问题吗? 如何解决呢?

谢谢。

1 个答案:

答案 0 :(得分:9)

键盘导致重建

完全有道理并且预期软件键盘打开导致重建。在幕后,MediaQueryupdated with view insets。这些 MediaQueryData.viewInsets 确保您的 UI 知道隐藏它的键盘。抽象地说,遮挡屏幕的键盘会导致窗口发生变化,并且大部分时间会导致您的 UI 发生变化,这需要对 UI 进行更改 - 重建。

我可以确信您在 Flutter 应用程序中使用了 Scaffold。与许多其他框架小部件一样,Scaffold 小部件依赖(参见 InheritedWidget)在 MediaQuery(从包含您的应用)使用 MediaQuery.of(context)
有关详细信息,请参阅 Window


这一切都归结为 Scaffold 依赖于视图插入。这允许它在这些视图插入发生变化时调整大小。基本上,当键盘打开时,视图插入更新,这允许脚手架在底部缩小,删除遮挡空间。

长话短说,适应调整后的视图插图的脚手架需要重新构建脚手架 UI。并且由于您的小部件必然是脚手架(可能是 body)的子代,因此您的小部件也会在发生这种情况时重建。

您可以使用 MediaQueryData 禁用视图插入调整大小行为。但是,这不一定会停止重建,因为可能仍然依赖于 MediaQuery。我将在下面解释你应该如何真正思考这个问题。

幂等构建方法

您应该始终以 build 方法幂等的方式构建 Flutter 小部件。
范例是构建调用可以在任何时间点发生,每秒最多 60 次(如果刷新率更高,则可能更多)。

我所说的幂等构建调用是指当您的小部件配置(在 Scaffold.resizeToAvoidBottomInset 的情况下)或您的状态(在 {{3} }) 更改,生成的小部件树应该完全相同。因此,您不想在 build 中处理任何状态 - 它的唯一职责应该是表示当前配置或状态。


导致重建的软件键盘打开只是一个很好的例子,说明了为什么会这样。其他示例包括旋转设备、在网络上调整大小,但随着您的小部件树开始变得复杂,它实际上可以是任何东西(更多内容见下文)。

StreamBuilder 在重建时重新订阅

回到最初的问题:在这种情况下,您的问题是您错误地接近 StreamBuilder。您不应该为它提供一个每次构建都重新创建的流。

流构建器的工作方式是订阅初始流,然后在更新流时重新订阅。这意味着当 stream 小部件的 StreamBuilder 属性在两次 build 调用之间不同时,流构建器将取消订阅第一个流并订阅第二个(新)流。

您可以在 StatelessWidgets 中看到这一点:

if (oldWidget.stream != widget.stream) {
  if (_subscription != null) {
    _unsubscribe();
    _summary = widget.afterDisconnected(_summary);
  }
  _subscribe();
}

这里显而易见的解决方案是,当您不想重新订阅时,您将希望在不同的构建调用之间提供相同的流。这可以追溯到幂等构建调用


例如,StreamController 将始终返回相同的流,这意味着在您的 stream: streamController.stream 中使用 StreamBuilder 是安全的。基本上,所有控制器、行为主体等实现都应该以这种方式运行 - 只要您不重新创建您的流,StreamBuilder 就会妥善处理它!

因此,您案例中的错误函数是 _bloc.inputObservable(),它每次都会创建一个新流,而不是返回相同的流。

注意事项

请注意,我说过构建调用可以“在任何时间点”发生。实际上,您可以(技术上)准确控制应用中每次构建的发生时间。然而,一个普通的应用程序会非常复杂,你可能无法控制它,因此,你会希望有幂等的构建调用。
导致重建的键盘就是一个很好的例子。

如果您从高层次考虑,这正是您想要的 - 框架及其小部件(或您创建的小部件)负责响应外部更改并在必要时进行重建。树中的叶小部件不应该关心是否发生重建 - 它们应该可以放置在任何环境中,并且框架会通过相应地重建来负责对该环境的更改做出反应。

我希望我能够为你解决这个问题:)