I'm building a tinder-like card swipe mechanism where I have a list of cards widget in a Stack
widget. The first card is wrapped inside a Dismissible
widget which, on dismiss, dismiss the top card and add a new one at the bottom.
My issue here is that, when the first card is dismissed and the second one becomes the first, that card is momentarily disposed and initState
is called again
But, as Linus Torvalds once said,
"Talk is cheap, show me the code"
so I put together a example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(color: Colors.white, child: CardStack()),
);
}
}
class CardStack extends StatefulWidget {
@override
_CardStackState createState() => _CardStackState();
}
class _CardStackState extends State<CardStack> {
final List<String> _postIds = ["1", "2", "3"];
int _nextId = 4;
_updatePosts(DismissDirection dir) {
setState(() {
_postIds.removeAt(0);
_postIds.add((_nextId++).toString());
});
}
List<Widget> _buildCards() {
List<Widget> cards = [];
for (int i = 0; i < _postIds.length; ++i) {
if (i == 0) {
cards.insert(
0,
Dismissible(
key: UniqueKey(),
onDismissed: _updatePosts,
child: Card(key: ValueKey(_postIds[i]), postId: _postIds[i]),
));
} else {
cards.insert(0, Card(key: ValueKey(_postIds[i]), postId: _postIds[i]));
}
}
return cards;
}
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: _buildCards(),
);
}
}
class Card extends StatefulWidget {
Card({Key key, this.postId}) : super(key: key);
final String postId;
@override
_CardState createState() => _CardState();
}
class _CardState extends State<Card> {
@override
void initState() {
super.initState();
print("Init state card with id ${widget.postId}");
}
@override
Widget build(BuildContext context) {
return Container(
height: 200,
width: 200,
color: Colors.blueAccent,
child: Center(
child: Text(widget.postId),
),
);
}
}
if I run that it logs
I/flutter (26740): Init state card with id 3
I/flutter (26740): Init state card with id 2
I/flutter (26740): Init state card with id 1
// swipe
I/flutter (26740): Init state card with id 4
I/flutter (26740): Init state card with id 2 <- unwanted rebuild
I think the rebuild of the first card is due to the fact that is is now wrapped inside the Dismissible
widget so flutter doesn't know how to reuse the underlying Card widget.
Is there a way to prevent this unwanted rebuild here?
Using setState to rebuild widgets Flutter gives you access to setState() . In this case, we have to ensure setState() has the new values. When setState() is called, Flutter will know to get these new values and mark the widget that needs to be rebuilt.
To create a stateful widget in a flutter, use the createState() method. The stateful widget is the widget that describes part of a user interface by building a constellation of other widgets that represent a user interface more concretely. A stateful Widget means a widget that has a mutable state.
The issue is, the location of your StatefulWidget
is changing inside the widget tree.
It could be that your StatefulWidget
is conditionally wrapped inside a specific widget, such that your widget tree varies between:
SomeWidget
MyStatefulWidget
and:
SomeWidget
AnotherWidget
MyStatefulWidget
By default, Flutter will not understand that MyStatefulWidget
moved inside the widget tree.
Instead, it'll think that the previous MyStatefulWidget
got removed, and the new one is completely unrelated to the original one.
This leads to dispose
being called on the previous State
, and initState
being called on the new State
.
To prevent this, you have a few solutions.
If you can, this is the best solution: Refactor your layout such that the widget tree is always the same.
For example, if you want to only the first widget in a ListView
to be dismissible, then instead of:
ListView.builder(
itemBuilder: (_, index) {
final child = MyStatefulWidget();
if (index == 0) {
return Dismissible(key: ValueKey(index), onDismissed: ..., child: child);
}
}
)
do:
ListView.builder(
itemBuilder: (_, index) {
return Dismissible(
key: ValueKey(someValue),
onDismissed: ...,
confirmDismiss: (_) async => index == 0, // only first item can be dismissed
child: MyStatefulWidget();
);
}
)
This way, the tree does not conditionally change, but the behavior is still conditional
Less ideal as more costly, but more flexible.
You can use GlobalKey
to tell Flutter that a specific widget can move from one location to another.
As such, we would be able to write:
Widget build(BuildContext context) {
if (something) {
return SomeTree(
child: MyStatefulWidget(key: GlobalObjectKey('my widget'));
);
} else {
return AnotherTree(
child: MyStatefulWidget(key: GlobalObjectKey('my widget'));
);
}
}
This way, while transitioning from SomeTree
to AnotherTree
, the widget MyStatefulWidget
will preserve its state.
As a little reminder, avoid it if possible. This is relatively costly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With