为什么软件键盘会在打开/关闭时导致小部件重建?

Ros*_*han 16 keyboard textfield dart flutter

我有一个屏幕,其中包含一个Form带有StreamBuilder. 当我从 加载初始数据时StreamBuilderTextFormField按预期显示数据。
当我点击 内部时TextFormField,软件键盘会出现,这会导致小部件重建。当键盘再次按下时,同样的情况再次发生。

不幸的是,StreamBuilder再次订阅了并且文本框值被替换为初始值。

这是我的代码:

@override
Widget build(BuildContext context) {
  return StreamBuilder(
    stream: _bloc.inputObservable(),
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        return TextFormField(
          // ...
        );
      }
      return const Center(
        child: CircularProgressIndicator(),
      );
    },
  );
}
Run Code Online (Sandbox Code Playgroud)

我该如何解决这个问题?

cre*_*not 16

导致重建的键盘

这是完全有道理的预计软件键盘打开会导致重建。在幕后,MediaQuery与更新视图插图。这些MediaQueryData.viewInsets确保您的 UI 知道键盘遮挡了它。抽象地说,键盘遮挡屏幕会导致窗口发生变化,并且大部分时间会导致您的 UI 发生变化,这需要对 UI 进行更改 - 重建。

我可以确信您Scaffold在 Flutter 应用程序中使用了 a 。像许多其他的框架部件,该Scaffold部件依赖(见InheritedWidget)上MediaQuery(从获取数据Window包含您的应用程序)使用MediaQuery.of(context)
有关MediaQueryData更多信息,请参阅。


这一切都归结为Scaffold对视图插入的依赖。这允许它在这些视图插入更改时调整大小。基本上,当键盘打开时,视图插入更新,这允许脚手架在底部收缩,去除被遮挡的空间。

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

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

幂等构建方法

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

我的意思幂等构建的呼叫是,当你的小部件的配置没有什么(在的情况下,StatelessWidget小号),或一无所知的状态(在的情况下,StatefulWidget小号)的变化,所产生的widget树应该是严格相同。因此,您不想处理任何状态build- 它的唯一职责应该是表示当前配置或状态。


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

StreamBuilder 重新订阅重建

回到最初的问题:在这种情况下,您的问题是您的方法StreamBuilder不正确。你应该给它是一个流重建每个构建。

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

您可以在_StreamBuilderBaseState.didUpdateWidget实现中看到这一点:

if (oldWidget.stream != widget.stream) {
  if (_subscription != null) {
    _unsubscribe();
    _summary = widget.afterDisconnected(_summary);
  }
  _subscribe();
}
Run Code Online (Sandbox Code Playgroud)

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


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

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

笔记

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

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

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