英文:
Stateless widget is strangely rebuilt
问题
以下是您要翻译的内容:
描述
我有一个有状态的小部件 W
。作为状态,它有一个列表 l = [S(), S()]
,其中 S
是一个无状态小部件。
当我在 setState()
内部交换 l
的元素时,会重新构建两个 S
。换句话说,现有的 S
没有被重用。
为什么?
具体示例
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme:
ThemeData(brightness: Brightness.dark, primaryColor: Colors.blueGrey),
home: W(),
debugShowCheckedModeBanner: false,
);
}
}
class W extends StatefulWidget {
const W({
super.key,
});
@override
State<W> createState() => _WState();
}
class _WState extends State<W> {
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
// List<ColorCard> l = [ColorCard(), ColorCard()];
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () =>
setState(() => {this.l.insert(1, this.l.removeAt(0))}),
child: Icon(Icons.swap_horiz)),
body: Center(
child: Row(
children: this.l,
)));
}
}
class Tile extends StatelessWidget {
final String text;
const Tile({required String this.text, super.key});
@override
Widget build(BuildContext context) {
return Stack(children: [
ColorCard(),
Text(this.text),
]);
}
}
Color createRandomColor() {
final colors = [
Colors.red,
Colors.blue,
Colors.yellow,
Colors.green,
Colors.purple,
];
return colors[Random().nextInt(colors.length)];
}
class ColorCard extends StatelessWidget {
final color = createRandomColor();
ColorCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: this.color,
);
}
}
有状态的小部件 W
具有以下状态:
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
并且交换 l
的元素使 Tile()
总是以某种原因重新构建,尽管 Tile
扩展了 StatelessWidget
。
Tile
具有以下结构:
Tile (无状态)
- Stack
- ColorCard (无状态)
- Container
- Text
更奇怪的是,当我在以下代码段中注释掉第一行并取消注释第二行时,ColorCard()
不会 被重建(DartPad)。
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
// List<ColorCard> l = [ColorCard(), ColorCard()];
给 Tile()
添加一个键解决了问题(DartPad)。
List<Tile> l = [
Tile(key: UniqueKey(), text: "hello"),
Tile(key: UniqueKey(), text: "world")
];
// List<ColorCard> l = [ColorCard(), ColorCard()];
尽管我认为根据 Keys! What are they good for? 的说法,不需要键,它说
> 如果您发现自己添加、删除或重新排序一组具有相同类型的小部件,这些小部件保存一些状态,那么使用键可能是您的未来选择。
为什么?
英文:
Description
I have a stateful widget W
. As a state, it has a list l = [S(), S()]
, where S
is a stateless widget.
When I swap the elements of l
inside setState()
two S
are rebuilt. In other words, the existing S
are not reused.
Why?
Concrete Example
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme:
ThemeData(brightness: Brightness.dark, primaryColor: Colors.blueGrey),
home: W(),
debugShowCheckedModeBanner: false,
);
}
}
class W extends StatefulWidget {
const W({
super.key,
});
@override
State<W> createState() => _WState();
}
class _WState extends State<W> {
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
// List<ColorCard> l = [ColorCard(), ColorCard()];
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () =>
setState(() => {this.l.insert(1, this.l.removeAt(0))}),
child: Icon(Icons.swap_horiz)),
body: Center(
child: Row(
children: this.l,
)));
}
}
class Tile extends StatelessWidget {
final String text;
const Tile({required String this.text, super.key});
@override
Widget build(BuildContext context) {
return Stack(children: [
ColorCard(),
Text(this.text),
]);
}
}
Color createRandomColor() {
final colors = [
Colors.red,
Colors.blue,
Colors.yellow,
Colors.green,
Colors.purple,
];
return colors[Random().nextInt(colors.length)];
}
class ColorCard extends StatelessWidget {
final color = createRandomColor();
ColorCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: this.color,
);
}
}
The stateful widget W
has the state
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
and swapping the elements of l
makes Tile()
always rebuilt for some reason though Tile
extends StatelessWidget
.
Tile
has this structure:
Tile (stateless)
- Stack
- ColorCard (stateless)
- Container
- Text
More strangely, when I comment the first line and uncomment the second line in the snippet below, then ColorCard()
is not rebuilt at all (DartPad).
List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
// List<ColorCard> l = [ColorCard(), ColorCard()];
Adding a key to Tile()
solves the problem (DartPad),
List<Tile> l = [
Tile(key: UniqueKey(), text: "hello"),
Tile(key: UniqueKey(), text: "world")
];
// List<ColorCard> l = [ColorCard(), ColorCard()];
though I thought keys aren't required as Keys! What are they good for? says
> If you find yourself adding, removing, or reordering a collection of widgets of the same type that hold some state, using keys is likely in your future.
Why?
答案1
得分: 2
以下是您要翻译的内容:
奇怪的部分来自于 canUpdate 检查,控件没有关键字时,同一类型数组中的每个元素都是相等的,这意味着 一切都可以更新,A(key: null) 等于 A(key: null),然后因为无状态组件 总是重建 每个父组件的 setState
都需要从每个未实例化的子组件中获取新的 build
。
这是否意味着它不能保持“状态”?嗯,还有其他检查(就像这个)来验证是否需要重建某些内容;在拖拽示例中,您可以看到 SquareCard.build
被调用,但没有任何变化。
在这个 pad 中,修复非常简单,实际效果是告诉 Flutter 这些小部件不相同并进行交换。
- const Tile({required String this.text, super.key});
+ Tile({required String this.text}) : super(key: ValueKey(text));
一个很好的例子是可拖动的对象,在这里有两种相同的 SquareCard
,“实例化” 不会改变,除非其中两个交换位置,但它们都以相同的颜色重新构建,树组件(带有 'Add / Remove')将在每个状态下始终重新构建(对于这一个使用关键字不会改变任何内容)。
(使用 'Add / Remove',要添加,将其拖到包装中的元素;要删除,将元素拖到它)
class DragPage extends StatefulWidget {
DragPage({super.key});
@override
State<DragPage> createState() => _DragPage();
}
// 将拖动映射到目标
typedef SrcDst = MapEntry<SquareCard, SquareCard>;
class _DragPage extends State<DragPage> {
late final cards = List<Widget>.generate(
5, (i) => SquareCard(text: 'Card $i', stream: _drag_stream)
);
final _drag_stream = StreamController<SrcDst>();
@override
void initState() {
_drag_stream.stream.listen(
(SrcDst ev) {
print('${ev.key} > ${ev.value}');
var widget_ph;
var src_index = cards.indexOf(ev.key);
var dst_index = cards.indexOf(ev.value);
if (src_index == -1) {
widget_ph = SquareCard(
text: 'Card ${cards.length}',
stream: _drag_stream,
);
} else {
widget_ph = cards.removeAt(
cards.indexOf(ev.key),
);
}
if (dst_index != -1) {
cards.insert(dst_index, widget_ph);
}
setState(() => null);
},
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 350,
child: Wrap(
children: cards,
)),
Center(
child: SquareCard(
text: 'Add / Remove',
stream: _drag_stream,
),
)
],
),
);
}
}
要注意的小细节是 toStringShort()
如果有关键字,则会显示使用的关键字,因此如果您添加一个,在构建时将显示是哪一个。
class SquareCard extends StatelessWidget {
final String text;
final StreamController<SrcDst> stream;
final Color color = Color(0xFF000000 | Random().nextInt(0xFFFFFF));
SquareCard({super.key, required this.text, required this.stream});
Widget build_card(BuildContext ctx, bool is_drag) {
return Container(
color: is_drag ? color.withAlpha(180) : color,
child: Center(child: Text(text)),
height: 100,
width: 100,
);
}
@override
Widget build(BuildContext ctx) {
print('Building ${this.toStringShort()}');
return DragTarget<SquareCard>(
builder: (ctx, a, r) {
return Draggable<SquareCard>(
feedback: Material(child: build_card(ctx, true)),
childWhenDragging: build_card(ctx, true),
child: build_card(ctx, false),
data: this,
);
},
onAccept: (SquareCard other) => stream.add(
SrcDst(other, this),
),
);
}
}
以及其余的代码。
import 'dart:math';
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: DragPage(),
),
);
}
}
请注意,您提供的代码片段包含许多程序代码和注释,我已经提取并翻译了其中的文本部分。如果您需要任何其他信息或有其他问题,请告诉我。
英文:
The strange part comes from the canUpdate check that widgets have, without a key, every element in an array of the same type is equal and that translates to everything can be updated, A(key: null) is equal to A(key: null), then because stateless components always rebuild every parent setState
will require a new build
from every non instantiated child.
Doesn't that mean that it can't hold a "state"?, well, there are other checks (like this one) to verify that there is a need to rebuild something; in the drag example you can see that SquareCard.build
is being called and nothing changes.
With that in this pad the fix is quite simple, the actual effect is that tells flutter that the widgets are not same and swaps them.
- const Tile({required String this.text, super.key});
+ Tile({required String this.text}) : super(key: ValueKey(text));
A great example for this would be draggable objects, here there are two "kinds" of the same SquareCard
, the instantiated wont get changed except when 2 of them swap places bought get rebuilt with the same color and the tree component (the one with 'Add / Remove') will always get rebuilt on every state (using a key wont change anything for this one).
(With 'Add / Remove', to add, drag it to an element on the wrap; to remove, drag the element to it)
class DragPage extends StatefulWidget {
DragPage({super.key});
@override
State<DragPage> createState() => _DragPage();
}
// Maps the dragged to the target
typedef SrcDst = MapEntry<SquareCard, SquareCard>;
class _DragPage extends State<DragPage> {
late final cards = List<Widget>.generate(
5, (i) => SquareCard(text: 'Card $i', stream: _drag_stream)
);
final _drag_stream = StreamController<SrcDst>();
@override
void initState() {
_drag_stream.stream.listen(
(SrcDst ev) {
print('${ev.key} > ${ev.value}');
var widget_ph;
var src_index = cards.indexOf(ev.key);
var dst_index = cards.indexOf(ev.value);
if (src_index == -1) {
widget_ph = SquareCard(
text: 'Card ${cards.length}',
stream: _drag_stream,
);
} else {
widget_ph = cards.removeAt(
cards.indexOf(ev.key),
);
}
if (dst_index != -1) {
cards.insert(dst_index, widget_ph);
}
setState(() => null);
},
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 350,
child: Wrap(
children: cards,
)),
Center(
child: SquareCard(
text: 'Add / Remove',
stream: _drag_stream,
),
)
],
),
);
}
}
A small thing to point here is that toStringShort()
prints out the key used if there was one, so if you add one, on the build it will show witch one was.
class SquareCard extends StatelessWidget {
final String text;
final StreamController<SrcDst> stream;
final Color color = Color(0xFF000000 | Random().nextInt(0xFFFFFF));
SquareCard({super.key, required this.text, required this.stream});
Widget build_card(BuildContext ctx, bool is_drag) {
return Container(
color: is_drag ? color.withAlpha(180) : color,
child: Center(child: Text(text)),
height: 100,
width: 100,
);
}
@override
Widget build(BuildContext ctx) {
print('Building ${this.toStringShort()}');
return DragTarget<SquareCard>(
builder: (ctx, a, r) {
return Draggable<SquareCard>(
feedback: Material(child: build_card(ctx, true)),
childWhenDragging: build_card(ctx, true),
child: build_card(ctx, false),
data: this,
);
},
onAccept: (SquareCard other) => stream.add(
SrcDst(other, this),
),
);
}
}
And the rest of the code.
import 'dart:math';
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: DragPage(),
),
);
}
}
答案2
得分: 1
每当你用setState
重建W
时,W
中的每个StatelessWidget
都会被重新构建。Stateless widgets并不是常量,它们是不可变的,只是意味着它们在创建并插入widget树之后是不可变的,并且没有内部的、可变的状态应该在该特定widget的UI中反映出来。
在Tile
的build
方法中调用ColorCard()
时,
@override
Widget build(BuildContext context) {
return Stack(children: [
ColorCard(),
Text(this.text),
]);
}
每次调用build
方法(通过_WState
中的setState
调用),ColorCard
widget的构造函数将被执行,导致随机颜色。这就是你在第一个版本中看到颜色变化的原因。
另一方面,在第二个版本中,当你取消注释时,
List<ColorCard> l = [ColorCard(), ColorCard()];
会发生不同的事情。一旦l
列表中的元素作为ColorCard
的实例创建,它们的构造函数运行,随机颜色就会分配给这两个ColorCard
widgets。使用setState
改变了这两个元素的顺序,但它们不会被重新创建,所以构造函数不会再次运行,颜色就不会改变。
如果你将代码更改为颜色不是在构造函数中分配给ColorCard
而是在build
方法中分配,你可以在第二个版本中轻松测试这种行为。将ColorCard
定义为以下形式:
class ColorCard extends StatelessWidget {
ColorCard({Key? key});
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: createRandomColor(),
);
}
}
现在,由于createRandomColor()
函数是从build
方法中调用的,第二个版本中的颜色也会改变。
英文:
Every StatelessWidget
within widget W
will be rebuilt when you rebuild W
with calling setState
, this is what Flutter does. Stateless widgets are not constants, being stateless means only that they are immutable after they are created and inserted into the widget tree and do not have an internal, changeable state that should be reflected in the UI of that particular widget.
Since calling ColorCard()
is within the build
method of Tile
,
@override
Widget build(BuildContext context) {
return Stack(children: [
ColorCard(),
Text(this.text),
]);
}
every time the build
method is called (and it will be called by using setState
in _WState
), the constructor of ColorCard
widget will be executed and will result in a random color. That's why you experience colors changing in the first version.
On the other hand side, in the second version, when you uncomment,
List<ColorCard> l = [ColorCard(), ColorCard()];
something different happens. The random colors are assigned to both ColorCard
widgets once they are created as elements in the l
list, once their constructors run. With setState
you change the order of the two elements but they are not re-created so the constructors don't run again, meaning the colors will not change.
You can easily test this behaviour in the second version if you change the code so that the color is not assigned to ColorCard
in the constructor but in the build
method. Define ColorCard
like this,
class ColorCard extends StatelessWidget {
ColorCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: createRandomColor(),
);
}
}
and you will have the colors changing in the second version as well, since the function createRandomColor()
is now called from the build
method.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论