4

I'm working on a Flutter app using Riverpod for the state management and go_router for the routing. I'm trying to make a view model for my screen, because my screen need 3 differents async state (a user, a board game, and another request to the database to know if the user owns the board game). My problem is the screen is not updated (I need to stop and restart the app to see modifications). With a print, I see that my screen is build the first time, but even I leave the screen, when I come back the widget is not build for a new time. The goal of my screen is to provide a button to add a game to user's games library, and if the game is already in the user's games library, a delete button.

I'm wondering if there is a way to remove the screen pushNamed with go_router (I already try pushNamedAndRemoveUntil method) or force the new build of the widget with Riverpod.

That is what I'm trying to do in my view_model:

final detailsGameStateProvider =
StateNotifierProvider.family<DetailsGameState, AsyncValue<BoardGameStoredData>, BoardGameDetails>((ref, details) => DetailsGameState(ref, details));

class DetailsGameState extends StateNotifier<AsyncValue<BoardGameStoredData>> {
  DetailsGameState(this.ref, this.details) : super(const AsyncValue.loading()){
    init(details);
  }

  final BoardGameDetails details;
  final Ref ref;

  Future<void> init(BoardGameDetails details) async {
    state = const AsyncValue.loading();
    try {
      final currentUser = await ref.watch(currentUserProvider.future);
      final boardGameDetails = await ref.watch(boardGameDetailsProvider(details).future);
      final check = await ref.watch(gameStoredCheckerProvider(BoardGameStored(user: currentUser!, boardGame: boardGameDetails!)).future);
      state = AsyncValue.data(BoardGameStoredData(user: currentUser, boardGame: boardGameDetails, isStored: check));
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }

  void saveGameToLibrary() async {
    state = const AsyncValue.loading();
    try {
      final currentUser = await ref.watch(currentUserProvider.future);
      final boardGameDetails = await ref.watch(boardGameDetailsProvider(details).future);
      await ref.watch(saveBoardGameProvider(BoardGameStored(user: currentUser!, boardGame: boardGameDetails)).future);
      state = AsyncValue.data(BoardGameStoredData(user: currentUser, boardGame: boardGameDetails, isStored: false));
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }

  void deleteGameFromLibrary() async {
    state = const AsyncValue.loading();
    try {
      final currentUser = await ref.watch(currentUserProvider.future);
      final boardGameDetails = await ref.watch(boardGameDetailsProvider(details).future);
      await ref.watch(deleteBoardGameProvider(BoardGameStored(user: currentUser!, boardGame: boardGameDetails)).future);
      state = AsyncValue.data(BoardGameStoredData(user: currentUser, boardGame: boardGameDetails, isStored: true));
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }
}

How I consume my view model in my screen:

class DetailsGameScreen extends StatefulHookConsumerWidget {
  const DetailsGameScreen({Key? key, required this.id, required this.title})
      : super(key: key);
  final String id;
  final String title;
  @override
  ConsumerState<DetailsGameScreen> createState() => _DetailsGameScreenState();
}

class _DetailsGameScreenState extends ConsumerState<DetailsGameScreen> {
  @override
  Widget build(BuildContext context) {
    final details = BoardGameDetails(id: widget.id, title: widget.title);
    return ref.watch(detailsGameStateProvider(details)).when(
          data: (boardGameDetailsData) {
            return Scaffold(
              ...
              Container(
                padding: const EdgeInsets.symmetric(vertical: 16.0),
                child: ElevatedButton(
                         onPressed: () => {
                           boardGameDetailsData.isStored ?
ref.read(detailsGameStateProvider(details).notifier).deleteGameFromLibrary() 
: ref.read(detailsGameStateProvider(details).notifier).saveGameToLibrary(),
                           context.go('/Games')
                         },
                         child: boardGameDetailsData.isStored ? 
                           const Text("Delete this game",
                             style: TextStyle(color: Colors.white)) : 
                           const Text("Add to my games",
                             style: TextStyle(color: Colors.white)),
                )),
            );
          },
          loading: () => const Center(
            child: CircularProgressIndicator(),
          ),
          error: (error, _) => Center(
            child: Text(error.toString(),
                style: const TextStyle(color: Colors.red)),
          ));     
  }
}

EDIT: If I use autoDispose on my StateNotifierProvider, I have the following error: Unhandled Exception: Bad state: Tried to use DetailsGameState after `dispose` was called and I see with a print my init method from my StateNotifier is not called if it's not the first time I go to the screen.

EDIT 2: I add videos to show how it works (or not). These videos are too heavy to post here so here's the following links :

On each video, you can see at first the search screen (we can search a game per name and it works). I click on a details screen and try to add / delete game from my games library. After that, I'm redirecting on my games library, where nothing change because it facing the same issue.

Thanks for help!

Collembole
  • 158
  • 2
  • 13
  • Are you saying your screen/widget doesn't rebuild even when you leave the screen? If yes, add `autodispose` to your provider. – Frank nike Oct 28 '22 at 22:37
  • It's exactly what I say, but when I use autoDispose on my StateNotifierProvider, I have the following error: ```Unhandled Exception: Bad state: Tried to use DetailsGameState after `dispose` was called``` – Collembole Oct 29 '22 at 06:02
  • Think i get it now. You call `init` only when `DetailsGameState` is instantiated, which is only once (i think). Which file created the `Unhandled Exception ` – Frank nike Oct 29 '22 at 13:48
  • The complete log is : ```[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Tried to use DetailsGameState after `dispose` was called.``` And yes, the init method is called only when DetailsGameState is instantiated, but when I try to call the method before the return of the widget for example, I have always a loading status instead of my page. And I add more StateNotifier method, I edit my post. – Collembole Oct 29 '22 at 16:15
  • Sorry if I'm asking too much, buy could you make a video? This issue should be to hard to figure out then solve. The video should also show the dispose error. – Frank nike Oct 29 '22 at 19:54
  • 1
    Hi @Franknike, I edit my post and you can check the hyperlinks to see the videos. – Collembole Oct 30 '22 at 07:50
  • Is your code open source or can you share it? I can see there's a problem but I'm not in the right mindset to fix/solve it by just looking at it, that's why I what to run it and see your error myself. – Frank nike Oct 30 '22 at 16:57
  • Sorry but I can't share the project, but I you need some code parts let me know – Collembole Oct 31 '22 at 08:25

1 Answers1

2

I found the solution with the following:

In my view model:

final detailsGameStateProvider =
StateNotifierProvider.family<DetailsGameState, AsyncValue<bool>, String>((ref, idFromAPI) {
  final check = ref.read(gameStoredCheckerProvider(idFromAPI));
  return DetailsGameState(ref, idFromAPI, check);
});

class DetailsGameState extends StateNotifier<AsyncValue<bool>> {
  DetailsGameState(this.ref, this.idFromAPI, this.isStored) : super(const AsyncValue.loading()){
    init();
  }

  final String idFromAPI;
  final Ref ref;
  final Future<bool> isStored;

  void init() async {
    state = const AsyncValue.loading();
    try {
      final check = await isStored;
      state = AsyncValue.data(check);
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }

  void saveGameToLibrary() async {
    state = const AsyncValue.loading();
    try {
      ref.read(saveBoardGameProvider(idFromAPI));
      state = const AsyncValue.data(true);
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }

  void deleteGameFromLibrary() async {
    state = const AsyncValue.loading();
    try {
      ref.read(deleteBoardGameProvider(idFromAPI));
      state = const AsyncValue.data(false);
    } catch (e) {
      state = AsyncValue.error(e);
    }
  }
}

In my screen:

class DetailsGameScreen extends StatefulHookConsumerWidget {
  const DetailsGameScreen({Key? key, required this.id}) : super(key: key);
  final String id;
  @override
  ConsumerState<DetailsGameScreen> createState() => _DetailsGameScreenState();
}

class _DetailsGameScreenState extends ConsumerState<DetailsGameScreen> {
  @override
  Widget build(BuildContext context) {
    final detailsGame = ref.watch(boardGameDetailsProvider(widget.id));
    return detailsGame.when(
      data: (boardGameDetailsData) {
        return Scaffold(
          body: ListView(
            children: [
                      ...
                      Container(
                          padding: const EdgeInsets.symmetric(vertical: 16.0),
                          child: ref
                              .watch(detailsGameStateProvider(
                                  boardGameDetailsData.idFromApi))
                              .when(
                                data: (isStored) {
                                  return ElevatedButton(
                                    onPressed: () async => {
                                      isStored
                                          ? ref
                                              .read(detailsGameStateProvider(
                                                      boardGameDetailsData
                                                          .idFromApi)
                                                  .notifier)
                                              .deleteGameFromLibrary()
                                          : ref
                                              .read(detailsGameStateProvider(
                                                      boardGameDetailsData
                                                          .idFromApi)
                                                  .notifier)
                                              .saveGameToLibrary(),
                                    },
                                    child: isStored
                                        ? const Text("Delete this game",
                                            style:
                                                TextStyle(color: Colors.white))
                                        : const Text("Add to my games",
                                            style:
                                                TextStyle(color: Colors.white)),
                                  );
                                },
                                loading: () => const Center(
                                  child: CircularProgressIndicator(),
                                ),
                                error: (error, _) => Center(
                                  child: Text(error.toString(),
                                      style:
                                          const TextStyle(color: Colors.red)),
                                ),
                              )),
                      Container(
                          padding: const EdgeInsets.symmetric(vertical: 16.0),
                          child: ElevatedButton(
                              onPressed: () => Navigator.pop(context),
                              style: ButtonStyle(
                                backgroundColor:
                                    MaterialStateProperty.all(Colors.grey),
                              ),
                              child: const Text("Back",
                                  style: TextStyle(color: Colors.white))))
                    ],
          ),
        );
      },
      loading: () => const Center(
        child: CircularProgressIndicator(),
      ),
      error: (error, _) => Center(
        child:
            Text(error.toString(), style: const TextStyle(color: Colors.red)),
      ),
    );
  }
}
Collembole
  • 158
  • 2
  • 13