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?
2
Answers
Every
StatelessWidget
within widgetW
will be rebuilt when you rebuildW
with callingsetState
, 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 thebuild
method ofTile
,every time the
build
method is called (and it will be called by usingsetState
in_WState
), the constructor ofColorCard
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,
something different happens. The random colors are assigned to both
ColorCard
widgets once they are created as elements in thel
list, once their constructors run. WithsetState
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 thebuild
method. DefineColorCard
like this,and you will have the colors changing in the second version as well, since the function
createRandomColor()
is now called from thebuild
method.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 newbuild
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.
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)
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.And the rest of the code.