Stateless widget 被奇怪地重建

huangapple go评论64阅读模式
英文:

Stateless widget is strangely rebuilt

问题

以下是您要翻译的内容:

描述

我有一个有状态的小部件 W。作为状态,它有一个列表 l = [S(), S()],其中 S 是一个无状态小部件。

当我在 setState() 内部交换 l 的元素时,会重新构建两个 S。换句话说,现有的 S 没有被重用。

为什么?

具体示例

DartPad

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

Stateless widget 被奇怪地重建

Tile 具有以下结构:

Tile (无状态)
- Stack
  - ColorCard (无状态)
    - Container
  - Text

更奇怪的是,当我在以下代码段中注释掉第一行并取消注释第二行时,ColorCard() 不会 被重建(DartPad)。

  List<Tile> l = [Tile(text: "hello"), Tile(text: "world")];
  // List<ColorCard> l = [ColorCard(), ColorCard()];

Stateless widget 被奇怪地重建

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? 的说法,不需要键,它说

> 如果您发现自己添加、删除或重新排序一组具有相同类型的小部件,这些小部件保存一些状态,那么使用键可能是您的未来选择。

Stateless widget 被奇怪地重建

为什么?

英文:

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

DartPad

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.

Stateless widget 被奇怪地重建

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()];

Stateless widget 被奇怪地重建

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.

Stateless widget 被奇怪地重建

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&lt;DragPage&gt; createState() =&gt; _DragPage();
}
// Maps the dragged to the target
typedef SrcDst = MapEntry&lt;SquareCard, SquareCard&gt;;
class _DragPage extends State&lt;DragPage&gt; {
late final cards = List&lt;Widget&gt;.generate(
5, (i) =&gt; SquareCard(text: &#39;Card $i&#39;, stream: _drag_stream)
);
final _drag_stream = StreamController&lt;SrcDst&gt;();
@override
void initState() {
_drag_stream.stream.listen(
(SrcDst ev) {
print(&#39;${ev.key} &gt; ${ev.value}&#39;);
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: &#39;Card ${cards.length}&#39;,
stream: _drag_stream,
);
} else {
widget_ph = cards.removeAt(
cards.indexOf(ev.key),
);
}
if (dst_index != -1) {
cards.insert(dst_index, widget_ph);
}
setState(() =&gt; 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: &#39;Add / Remove&#39;,
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&lt;SrcDst&gt; 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(&#39;Building ${this.toStringShort()}&#39;);
return DragTarget&lt;SquareCard&gt;(
builder: (ctx, a, r) {
return Draggable&lt;SquareCard&gt;(
feedback: Material(child: build_card(ctx, true)),
childWhenDragging: build_card(ctx, true),
child: build_card(ctx, false),
data: this,
);
},
onAccept: (SquareCard other) =&gt; stream.add(
SrcDst(other, this),
),
);
}
}

And the rest of the code.

import &#39;dart:math&#39;;
import &#39;dart:async&#39;;
import &#39;package:flutter/material.dart&#39;;
void main() =&gt; 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中反映出来。

Tilebuild方法中调用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&lt;ColorCard&gt; 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.

huangapple
  • 本文由 发表于 2023年5月10日 18:28:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76217329.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定