I’m using Riverpod in my Flutter application, and I’ve run into an issue where my ConsumerWidget
isn’t rebuilding when changes occur in a nested provider.
I have a BankrollListScreen
which watches the bankrollListControllerProvider
. Inside the build method, I loop through a list of Bankroll
objects and for each Bankroll
, I want to display its total which I calculate using the bankrollTotalProvider
.
// this is simplified version of my code
class BankrollListScreen extends ConsumerWidget {
const BankrollListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<Bankroll>> bankrollListValue =
ref.watch(bankrollListControllerProvider);
return bankrollListValue.when(
data: (bankrollList) => ListView(
children: [
for (var bankroll in bankrollList)
GameSessionListTile(
bankrollTotal: Text(
ref.watch(
bankrollTotalProvider(bankroll.id),
).toString(),
),
onTap: () {
context.pushNamed(
AppRoute.gameSessionList.name,
pathParameters: {
'id': bankroll.id,
'bankrollName': bankroll.name!
},
);
},
),
],
),
// ... [Consider handling other cases for the AsyncValue]
);
}
}
Here’s how I calculate the bankrollTotal
:
@riverpod
Future<double> bankrollTotal(
BankrollTotalRef ref,
String bankrollId,
) async {
final gameSessionList = await ref
.watch(gameSessionRepositoryProvider)
.getGameSessionListForBankroll(bankrollId);
final depositList = await ref
.watch(depositRepositoryProvider)
.getDepositListForBankroll(bankrollId);
final gameSessionTotal = gameSessionList.fold(
0.0, (sum, session) => sum + (session.cashout ?? 0.0));
final depositTotal =
depositList.fold(0.0, (sum, deposit) => sum + (deposit.amount));
final totalBankroll = gameSessionTotal + depositTotal;
return totalBankroll;
}
The problem I’m facing is when bankrollTotalProvider
changes, it doesn’t invoke the build
method of the BankrollListScreen
as it’s only being watched by bankrollListControllerProvider
.
How can I make my BankrollListScreen
rebuild when there are any changes in bankrollTotalProvider
?
Possible solution?
I believe I’ve found a solution.
The ref.watch
will invoke the build
method when the state of a provider changes. The issue with my code is that the state of bankrollTotalProvider
doesn’t alter unless I intervene manually.
Therefore, in this scenario, instead of monitoring the provider, we should create another widget that triggers the method manually.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:today_poker/features/statistics/service/bankroll_stat_service.dart';
class BankrollTotalTextWidget extends ConsumerWidget {
final String bankrollId;
const BankrollTotalTextWidget({super.key, required this.bankrollId});
Future<String> fetchBankrollTotal(WidgetRef ref) async {
final value = await ref
.watch(bankrollStatServiceProvider(bankrollId).notifier)
.getBankrollTotal(bankrollId);
return value.toString();
}
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<String> bankrollTotalFuture = fetchBankrollTotal(ref);
return FutureBuilder<String>(
future: bankrollTotalFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data!);
} else if (snapshot.hasError) {
print(snapshot.error);
return Text("Error: ${snapshot.error}");
}
}
// While data is loading:
return const CircularProgressIndicator();
},
);
}
}
Then, I pass BankrollTotalTextWidget
to bankrollTotal
.
bankrollTotal: BankrollTotalTextWidget(bankroll.id),
2
Answers
First, by reading your business logic (to which I’m basically unaware of) it’s weird to see that
bankrollListControllerProvider
andbankrollTotalProvider
are unrelated. I would expected a reactive dependency, such thatbankrollTotalProvider
watchesbankrollListControllerProvider
to compute its thing. But I see that’s not the case.Anyways…
Something’s wrong here. I’d suggest giving your list elements a
key
(e.g. based onbankroll.id
) so that you can better debug why and how those elements are replaced by the framework.You’ve correctly registered
bankrollTotalProvider(id)
as a dependency forBankrollListScreen
, therefore this should definitely work.A note on your "possible solution"
Have you tried to just use
final total = ref.watch(bankrollTotalProvider(id))
in thatBankrollTotalTextWidget
, instead? It basically does the same thing, except you won’t use aFutureProvider
, but the usualAsyncValue
instead.If that works, it means you’ve encountered a classic Flutter-ish "builder" problem. See the docs.
You seem to be updating the state of your
bankrollListControllerProvider
provider incorrectly. Try usingList.of
on update, or use the spread operator like this:This will indicate to your
bankrollListControllerProvider
that its state has indeed been updated. All collections should use this state update approach. With simple types, you can do a simple assignmentstate = newState;