Search code examples
flutterdartproviderflutter-providerriverpod

Flutter: Riverpod rebuild all elements in ListView.builder


Recently I started to learn riverpod and provider as state management method. Therefore I wanted to rebuild my current application.

My Problem

In my application I have a list (ListView.builder) which displays multiple cards. When a card is clicked (GestureDetector), it should be flipped. This worked with the previous state management (setState()) without any problems. When using riverpod, all cards in the ListView are now flipped when clicking on one card.

Here is my code:

Provider

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

@immutable
class TestMovieCard {
  const TestMovieCard({
    required this.id,
    required this.title,
  });

  final String id;
  final String title;
}

class MoviesNotifier extends StateNotifier<List<TestMovieCard>> {
  MoviesNotifier()
      : super([
          const TestMovieCard(id: "1", title: "Im Westen nichts Neues"),
          const TestMovieCard(
              id: "2", title: "Star Wars Das Erwachen der Macht")
        ]);

  double _flipAngle = 0;
  double get flipAngle => _flipAngle;

  void flipMovieCard() {
    _flipAngle = (_flipAngle + pi) % (2 * pi);
    state = [...state];
  }

  bool checkIfCardIsFront(double animationValue) {
    if (animationValue >= (pi / 2)) {
      return false;
    } else {
      return true;
    }
  }
}

final movieCardProvider =
    StateNotifierProvider<MoviesNotifier, List<TestMovieCard>>((ref) {
  return MoviesNotifier();
});

Widget

class Test extends ConsumerWidget {
  const Test({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Size size = MediaQuery.of(context).size;
    List<TestMovieCard> movieCards = ref.watch(movieCardProvider);

    return DefaultTabController(
      length: 2,
      child: Scaffold(
        body: Container(
            padding: const EdgeInsets.all(defaultPadding),
            child: Column(
              children: [
                SizedBox(
                  height: size.height * 0.05,
                ),
                Expanded(
                  child: SizedBox(
                      width: double.infinity,
                      child: TabBarView(children: [
                        MediaQuery.removePadding(
                            removeTop: true,
                            context: context,
                            child: ListView.builder(
                                itemCount: movieCards.length,
                                itemBuilder: (context, index) {
                                  return GestureDetector(
                                    // key: Key(movieCards[widget.listIndex].id),
                                    onTap: () {
                                      ref
                                          .watch(movieCardProvider.notifier)
                                          .flipMovieCard();
                                    },
                                    child: TweenAnimationBuilder(
                                        tween: Tween<double>(
                                            begin: 0,
                                            end: ref
                                                .watch(
                                                    movieCardProvider.notifier)
                                                .flipAngle),
                                        duration: const Duration(seconds: 1),
                                        builder: (BuildContext context,
                                            double val, __) {
                                          return (Transform(
                                              alignment: Alignment.center,
                                              transform: Matrix4.identity()
                                                ..setEntry(3, 2, 0.001)
                                                ..rotateY(val),
                                              child: ref
                                                      .watch(movieCardProvider
                                                          .notifier)
                                                      .checkIfCardIsFront(val)
                                                  ? Card(
                                                      elevation: 5,
                                                      shape: const RoundedRectangleBorder(
                                                          borderRadius:
                                                              BorderRadius.all(
                                                                  Radius.circular(
                                                                      20.0))),
                                                      child: Container(
                                                          height: size.height *
                                                              0.25,
                                                          decoration:
                                                              const BoxDecoration(
                                                            borderRadius:
                                                                BorderRadius.all(
                                                                    Radius.circular(
                                                                        20.0)),
                                                          ),
                                                          child: const Center(
                                                            child:
                                                                Text("Front"),
                                                          )))
                                                  : Transform(
                                                      alignment:
                                                          Alignment.center,
                                                      transform:
                                                          Matrix4.identity()
                                                            ..rotateY(pi),
                                                      child: Card(
                                                          elevation: 5,
                                                          shape: const RoundedRectangleBorder(
                                                              borderRadius: BorderRadius
                                                                  .all(Radius
                                                                      .circular(
                                                                          20.0))),
                                                          child: Container(
                                                            height:
                                                                size.height *
                                                                    0.25,
                                                            width:
                                                                double.infinity,
                                                            decoration:
                                                                const BoxDecoration(
                                                              borderRadius:
                                                                  BorderRadius
                                                                      .all(
                                                                Radius.circular(
                                                                    20.0),
                                                              ),
                                                            ),
                                                            child: const Center(
                                                              child:
                                                                  Text("Back"),
                                                            ),
                                                          )),
                                                    )));
                                        }),
                                  );
                                })),
                        Placeholder()
                      ])),
                )
              ],
            )),
        bottomNavigationBar: const CustomBottomNavigationBar(),
      ),
    );
  }
}

I hope i made my point clear. Thank you in advance.

I have already tried to implement the consumer in several places, but the state of all list items is always reloaded.


EDIT: New Provider code based on the help of Axel

@immutable
class TestMovieCard {
  TestMovieCard({
    required this.id,
    required this.title,
  });

  final String id;
  final String title;
  double flipAngle = 0;

  void flip() {
    flipAngle = (flipAngle + pi) % (2 * pi);
  }
}

class MoviesNotifier extends StateNotifier<List<TestMovieCard>> {
  MoviesNotifier()
      : super([
          TestMovieCard(id: "1", title: "Im Westen nichts Neues"),
          TestMovieCard(
            id: "2",
            title: "Star Wars Das Erwachen der Macht",
          )
        ]);

  void flipMovieCard(String idToFlip) {
    state = [
      ...state.where((x) => x.id != idToFlip),
      ...state.where((x) => x.id == idToFlip).flip()
    ];
  }

  bool checkIfCardIsFront(double animationValue) {
    if (animationValue >= (pi / 2)) {
      return false;
    } else {
      return true;
    }
  }
}

final movieCardProvider =
    StateNotifierProvider<MoviesNotifier, List<TestMovieCard>>((ref) {
  return MoviesNotifier();
});

Solution

  • You should have the _flipAngle attribute in your TestMovieCard so you can distinguish which cards must be flipped and which not.

    Then, once you call flipMovieCard, pass the card index to flip only the selected card. The code should be something like:

      void flipMovieCard(int index) {
        state[index].flipAngle = (state[index].flipAngle + pi) % (2 * pi);
      }
    

    Edit post OP's comment:

    The reason why the widget is not rebuilding automatically is because you need to recreate the entire list.

    You could implement a flip method in the TestMovieCard class and then do this:

    TestMovieCard flippedCard = state.where((x) => x.id == idToFlip).first;
    flippedCard.flip();
    state = [...state.where((x) => x.id != idToFlip), flippedCard];
    

    Instead of:

    state[index].flipAngle = (state[index].flipAngle + pi) % (2 * pi);