1

I'm trying to build a system just like tinder where there is a stack of cards a user can swipe and the next few cards are always preloaded.

To do that, I have a Stack widget that build a series of cards child widgets and give them the id of the content to load:

class PostCardStack extends StatefulWidget {
  const PostCardStack({Key key, this.postIds, this.onCardDismissed})
      : super(key: key);

  final List<String> postIds;
  final Function onCardDismissed;

  @override
  _PostCardStackState createState() => _PostCardStackState();
}

class _PostCardStackState extends State<PostCardStack> {
  ValueNotifier<double> _notifier = ValueNotifier<double>(0.0);

  _buildCardStack() {
    List<Widget> cards = [];
    for (String postId in widget.postIds) {
      int idx = widget.postIds.indexOf(postId);
      if (postId == widget.postIds.first) {
        cards.add(CustomDismissible(
            resizeDuration: null,
            dismissThresholds: {CustomDismissDirection.horizontal: 0.2},
            notifier: _notifier,
            key: Key(postId),
            onDismissed: (direction) {
              _notifier.value = 0.0;
              widget.onCardDismissed(postId);
            },
            child: SlidablePanel(
              panel: AnimatedBuilder(
                  animation: _notifier,
                  child: PostCard(
                    postId: postId,
                  ),
                  builder: (context, _) {
                    return Opacity(
                        opacity: 1 - _notifier.value,
                        child: PostCard(
                          postId: postId,
                        ));
                  }),
            )));
      } else {
        cards.add(AnimatedBuilder(
            animation: _notifier,
            child: PostCard(
              postId: postId,
            ),
            builder: (context, _) {
              return Opacity(
                  opacity: lerpDouble(1 - (0.1 * idx), 1 - ((0.1 * idx) - 0.1),
                      _notifier.value),
                  child: Transform(
                      origin: null,
                      alignment: Alignment.bottomCenter,
                      transform: Matrix4.translationValues(
                          0.0,
                          lerpDouble(
                              -idx * 35, (-idx * 35 + 35), _notifier.value),
                          0.0)
                        ..scale(
                            lerpDouble(1 - (0.1 * idx), 1 - ((0.1 * idx) - 0.1),
                                _notifier.value),
                            lerpDouble(1 - (0.1 * idx), 1 - ((0.1 * idx) - 0.1),
                                _notifier.value),
                            1),
                      child: PostCard(
                        postId: postId,
                      )));
            }));
      }
    }
    return cards.reversed.toList();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
        alignment: Alignment.bottomCenter, children: _buildCardStack());
  }
}

In the PostCard widget, I use the postId param to fetch the card information and build it when it is ready.

class PostCard extends StatefulWidget {
  const PostCard({Key key, this.postId}) : super(key: key);

  final String postId;

  @override
  _PostCardState createState() => _PostCardState();
}

class _PostCardState extends State<PostCard> {
  PostModel post;

  @override
  void initState() {
    super.initState();
    print("${widget.postId} mounted");
    _fetchPost(widget.postId);
  }

  @override
  void dispose() {
    print("${widget.postId} disposed");
    super.dispose();
  }

  _fetchPost(String postId) async {
    PostModel fullPost = await blablaFirestore(postId);

    setState(() {
      post = fullPost;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 300,
      child: Align(
        alignment: Alignment.topCenter,
        child: Text(post == null ? "Loading..." : post.text),
      ),
    );
  }
}

Everything works fine except when any element of the list of id in the Stack component changes, all the child cards gets rebuilt, therefore loosing their previously loaded state and have to fetch the data again.

If only one element of the list of id change, why are every cards rebuilding? Am I missing something here? :)

Edit: In the question marked as duplicate, the problem is the build method having side effects. I don't believe that this is the case here. In my case, the problem is that state of the PostCards widget is not kept even tho they rebuild with the exact same param (postId)

Cheers!

Théo Champion
  • 1,701
  • 1
  • 21
  • 46

1 Answers1

2

When setState is called the build function is called again, and all the PostCard widget will be recreated since they are created in the build function.

This said, if PostCard is a Stateful widget the state should not be destroyed (e.g. initState will not be called on them). But you will likely loose the reference to them, which might be why your code is not behaving as you expect (hard to tell from your code).

Maybe you should make the widget where this piece of code is called a Stateful widget and create a List<PostCard> postCards variable to store your cards, this way you can initialize postCards in your initState function and they will not be recreated when you call setState, thus preserving the reference.

Quentin
  • 758
  • 9
  • 11
  • The code of `Postcard` is really straight forward, It just fetch the card info from the provided `postId` in it's initstate and when it's ready render them in the card instead of a `CircularProgressIndicator`. The weird thing is that they appear to be destroyed (the dispose method is called on every parent rerender) – Théo Champion Oct 09 '19 at 10:24
  • Did you try to make the widget a Stateful widget? That should solve your issue. – Quentin Oct 10 '19 at 13:35