在 Flutter 中临时从树中移除时保留小部件状态

Preserve widget state when temporarily removed from tree in Flutter

我正在尝试保留小部件的状态,这样如果我暂时从小部件树中删除有状态小部件,然后稍后重新添加它,小部件将具有与之前相同的状态在我删除它之前。这是我的一个简化示例:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool showCounterWidget = true;
  @override
  Widget build(BuildContext context) {

    return Material(
      child: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            showCounterWidget ? CounterButton(): Text("Other widget"),
            SizedBox(height: 16,),
            FlatButton(
              child: Text("Toggle Widget"),
              onPressed: (){
                setState(() {
                showCounterWidget = !showCounterWidget;
              });
                },
            )
          ],
        ),
      ),
    );
  }
}

class CounterButton extends StatefulWidget {
  @override
  _CounterButtonState createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      color: Colors.orangeAccent,
      child: Text(counter.toString()),
      onPressed: () {
        setState(() {
          counter++;
        });
      },
    );
  }
}

理想情况下,我不希望状态重置,因此计数器不会重置为 0,我将如何保留我的计数器小部件的状态?

小部件旨在在其范围和生命周期内存储自己的瞬时数据。

根据您提供的内容,您正在尝试重新创建 CounterButton 子窗口小部件 ,方法是将其删除并重新添加到窗口小部件树中。

在这种情况下,CounterButton 下的计数器值未保存未保存 MyHomePage 屏幕,父小部件,没有任何对视图模型的引用或顶层内部或顶层的任何状态管理。

更技术性的概述 Flutter 如何呈现您的小部件

有没有想过如果您尝试为小部件创建构造函数,key 是什么?

class CounterButton extends StatefulWidget {
  const CounterButton({Key key}) : super(key: key);

  @override
  _CounterButtonState createState() => _CounterButtonState();
}

keys (key) 是 Fl​​utter 框架自动处理和使用的标识符,用于区分 widget 树中的 widget 实例。 删除添加小部件树中的小部件(CounterButton)会重置分配给它的key,因此数据它持有,它的状态也被删除。

NOTE: No need to create constructors for the a Widget if it will only contain key as its parameter.

来自文档:

Generally, a widget that is the only child of another widget does not need an explicit key.

为什么 Flutter 会更改分配给 CounterButton 的键?

你在CounterButtonStatefulWidget之间切换,TextStatelessWidget,Flutter识别这两个对象完全不同的原因.

您始终可以使用 Dart Devtools 检查更改并切换 Flutter 应用程序的行为。

关注_CounterButtonState结尾的#3a4d2

这是切换小部件后的小部件树结构。从 CounterButtonText 小部件。

您现在可以看到以 #31a53 结尾的 CounterButton 与之前的标识符不同,因为这两个小部件 完全不同

你会做什么?

我建议您将在运行时更改的数据保存在 _MyHomePageState 中,并在 CounterButton 中创建一个带有回调函数的构造函数来更新调用小部件中的值。

counter_button.dart

class CounterButton extends StatefulWidget {
  final counterValue;
  final VoidCallback onCountButtonPressed;

  const CounterButton({Key key, this.counterValue, this.onCountButtonPressed})
      : super(key: key);

  @override
  _CounterButtonState createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      color: Colors.orangeAccent,
      child: Text(widget.counterValue.toString()),
      onPressed: () => widget.onCountButtonPressed(),
    );
  }
}

假设您在 _MyHomePageState 中将变量命名为 _counterValue,您可以像这样使用它:

home_page.dart

_showCounterWidget
    ? CounterButton(
        counterValue: _counterValue,
        onCountButtonPressed: () {
          setState(() {
            _counterValue++;
          });
        })
    : Text("Other widget"),

此外,此解决方案将帮助您在应用的其他部分重复使用 CounterButton 或其他类似小部件。

我在 dartpad.dev 中添加了完整的示例。

Andrew 和 Matt 就 Flutter 如何在幕后渲染小部件进行了精彩的演讲:

进一步阅读

正如 Joshua 所说,小部件在暂时从树中移除时失去状态的原因是因为它失去了 Element/State。

现在你可能会问:

Can't I cache the Element/State so that next time the widget is inserted, it reuse the previous one instead of creating them anew?

这是一个正确的想法,但不是。你不能。 Flutter 将其判断为反模式,并在这种情况下抛出异常。

您应该做的是将小部件保留在小部件树中,处于禁用状态。

要实现这样的目标,您可以使用以下小部件:

  • IndexedStack
  • Visibility/Offstage

这些小部件将允许您将小部件保留在小部件树中(以便它保持其状态),但禁用它 rendering/animations/semantics。

因此,而不是:

Widget build(context) {
  if (condition)
    return Foo();
  else
    return Bar();
}

这会使 Foo/Bar 在它们之间切换时失去它们的状态

做:

IndexedStack(
  index: condition ? 0 : 1, // switch between Foo and Bar based on condition
  children: [
    Foo(),
    Bar(),
  ],
)

使用此代码,然后 Foo/Bar 不会 在它们之间来回移动时失去它们的状态。

这个问题的真正解决方案是状态管理。有几个很好的解决方案可以作为概念和 flutter 包使用。我个人经常使用 BLoC 模式。

原因是小部件状态旨在用于 UI 状态,而不是应用程序状态。 UI 状态主要是动画、文本输入或其他不会持续存在的状态。

问题中的示例是应用程序状态,因为它的持续时间比小部件的生存时间长。

关于创建基于 BLoC 的计数器有一点 Tutorial,这可能是一个很好的起点。