在 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
) 是 Flutter 框架自动处理和使用的标识符,用于区分 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 的键?
你在CounterButton
和StatefulWidget
之间切换,Text
是StatelessWidget
,Flutter识别这两个对象完全不同的原因.
您始终可以使用 Dart Devtools 检查更改并切换 Flutter 应用程序的行为。
关注_CounterButtonState
结尾的#3a4d2
。
这是切换小部件后的小部件树结构。从 CounterButton
到 Text
小部件。
您现在可以看到以 #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,这可能是一个很好的起点。
我正在尝试保留小部件的状态,这样如果我暂时从小部件树中删除有状态小部件,然后稍后重新添加它,小部件将具有与之前相同的状态在我删除它之前。这是我的一个简化示例:
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
) 是 Flutter 框架自动处理和使用的标识符,用于区分 widget 树中的 widget 实例。 删除和添加小部件树中的小部件(CounterButton
)会重置分配给它的key
,因此数据它持有,它的状态也被删除。
NOTE: No need to create constructors for the a
Widget
if it will only containkey
as its parameter.
来自文档:
Generally, a widget that is the only child of another widget does not need an explicit key.
为什么 Flutter 会更改分配给 CounterButton 的键?
你在CounterButton
和StatefulWidget
之间切换,Text
是StatelessWidget
,Flutter识别这两个对象完全不同的原因.
您始终可以使用 Dart Devtools 检查更改并切换 Flutter 应用程序的行为。
关注_CounterButtonState
结尾的#3a4d2
。
这是切换小部件后的小部件树结构。从 CounterButton
到 Text
小部件。
您现在可以看到以 #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,这可能是一个很好的起点。