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!
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)),
),
);
}
}