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

Posted

技术标签:

【中文标题】在 Flutter 中从树中临时移除时保留小部件状态【英文标题】:Preserve widget state when temporarily removed from tree in Flutter 【发布时间】:2020-04-18 08:25:03 【问题描述】:

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

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,我将如何保留计数器小部件的状态?

【问题讨论】:

您的问题是每次调用构建时都会创建 CountButton,请考虑在另一个范围内创建它并将其添加到构建中,而不是像 CounterButton() 那样从头开始创建它; 【参考方案1】:

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

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

在这种情况下,MyHomePage 屏幕(父小部件)中CounterButton 下的计数器值未保存未保存 ,不涉及视图模型或顶层内部或顶层的任何状态管理。

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

如果您尝试为小部件创建构造函数,是否想知道 key 是什么?

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

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

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

注意:如果 Widget 仅包含 key 作为其参数,则无需为它创建构造函数。

来自文档:

通常,作为另一个小部件的唯一子级的小部件不需要显式键。

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

您在CounterButton(即StatefulWidget)和Text(即StatelessWidget)之间切换,这就是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 如何在后台渲染小部件进行了精彩的演讲:

https://www.youtube.com/watch?v=996ZgFRENMs

进一步阅读

https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208 https://api.flutter.dev/flutter/widgets/Widget/key.html

【讨论】:

那是题外话。问题不是“我为什么要失去我的状态”,而是“当自愿从树中移除状态时如何保持状态”。我希望那些来到这里的人知道这些事实,并正在寻找其他东西。 嗨,Rémi,感谢您分享您的想法。我认为回答“为什么”与 OP 的“如何”密切相关。根据 OP 的问题,他显然是在“尝试”保留小部件状态。我只是假设他可能不熟悉小部件渲染的工作原理,如果我尝试更深入地了解为什么会发生这种行为,它也可能会有所帮助。感谢你的时间,雷米。谢谢。【参考方案2】:

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

现在你可能会问:

我不能缓存元素/状态,以便下次插入小部件时,它会重用前一个而不是重新创建它们吗?

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

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

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

索引堆栈 可见性/舞台外

这些小部件将允许您在小部件树中保留一个小部件(以便它保持其状态),但禁用其渲染/动画/语义。

因此,而不是:

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 有一定的了解,我不知道从小部件树中删除小部件时会丢失它们的状态,即使小部件之前已实例化。也许应该在“第一步”指南中注明。【参考方案3】:

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

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

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

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

【讨论】:

以上是关于在 Flutter 中从树中临时移除时保留小部件状态的主要内容,如果未能解决你的问题,请参考以下文章

在小部件树中检测到 Flutter Duplicate GlobalKey

Flutter/Dart:我的小部件树中可以有多个带有状态的小部件吗?

Flutter:导航时“小部件树中的多个全局键”

Flutter:检测任何在屏幕上不可见但在小部件树中的小部件的重建

Flutter:在从小部件树中删除时或在其生命周期结束时为小部件设置动画?

导航抽屉未在颤动中从 APPBar 小部件打开