riverpod
riverpod copied to clipboard
Invalidating an AutoDisposeProvider A that watches AutoDisposeProvider B doesn't dispose AutoDisposeProvider B automatically.
Describe the bug Let's say I have a Widget that depends on FutureProvider A, and then FutureProvider A depends on FutureProvider B. The Widget doesn't know about FutureProvider B. If FutureProvider B returns an error, FutureProvider A will return an error as well to the Widget. When the Widget got an Error, it would show an option to refresh by calling WidgetRef.invalidate() on FutureProvider A.
I assumed when FutureProvider A got invalidated, it would get disposed, then since only FutureProvider A depends on/watches FutureProvider B, FutureProvider B would get disposed of as well, resulting in a recomputed value for FutureProvider B.
But in Riverpod 2.5.0 that I'm using in my project, it still return the previous error/value.
To Reproduce
This is the code that I use to reproduce it:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
const wholeSentence = 'a sentence + $number';
const number = 100;
void main() {
test('testing future provider', () async {
final container = createContainer();
// First throw an exception
container.updateOverrides([listenedProvider.overrideWith((ref) => throw Exception())]);
await expectLater(
container.read(listeningProvider.future),
throwsA(isA<Exception>()),
);
// Then expect to get new value after invalidate
container.updateOverrides([listenedProvider.overrideWith((ref) async => number)]);
container.invalidate(listeningProvider);
await expectLater(
container.read(listeningProvider.future),
completion(wholeSentence),
);
});
testWidgets('testing widget with future provider', (widgetTester) async {
await widgetTester.pumpWidget(
ProviderScope(
overrides: [
listenedProvider.overrideWith((ref) {
print('throwing Exception');
throw Exception();
}),
],
child: const SomeWidget(),
),
);
print('setting up container');
final element = widgetTester.element(find.byType(SomeWidget));
final container = ProviderScope.containerOf(element);
print('running first test');
await expectLater(
container.read(listeningProvider.future),
throwsA(isA<Exception>()),
);
print('invalidate then get new value from listenedProvider');
// container.invalidate(listenedProvider);
container.invalidate(listeningProvider);
container.updateOverrides([listenedProvider.overrideWith((ref) async => number)]);
await widgetTester.pumpAndSettle();
await expectLater(
container.read(listeningProvider.future),
completion(wholeSentence),
);
});
}
class SomeWidget extends ConsumerWidget {
const SomeWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final result = ref.watch(listeningProvider);
if (result.hasValue) {
return MaterialApp(home: Text(result.value!));
}
return Container();
}
}
final listenedProvider = FutureProvider.autoDispose<int>((ref) async {
throw UnimplementedError();
});
final listeningProvider = FutureProvider.autoDispose<String>((ref) async {
final number = await ref.watch(listenedProvider.future);
return 'a sentence + $number';
});
ProviderContainer createContainer() {
final container = ProviderContainer(
overrides: [
listenedProvider.overrideWith((ref) async {
return number;
}),
],
);
addTearDown(() => container.dispose());
return container;
}
Expected behavior I expected both the unit test and widget test above should pass successfully. But in reality, it still throws an error. Only when I added ref.invalidate(listenedProvider) would it pass. But I don't want that, since it shouldn't know about listenedProvider/FutureProvider B
pinging @rrousselGit in case you missed this one. is this an intended behaviour? if yes, what's the appropriate approach for that goal?
facing same issue. what's the recommended way to handle this ?
That's totally normal. Invalidating a provider does not invalidate its dependencies. If you want to invalidate those, invalidate them explicitly.
If those are "private", consider refactoring your "A" provider to a notifier and have it expose a custom invalidate
method, then call ref.read(aProvider.notifier).invalidate()
– which would then internally call ref.invalidate(bProvider)
.
That's totally normal. Invalidating a provider does not invalidate its dependencies. If you want to invalidate those, invalidate them explicitly.
If those are "private", consider refactoring your "A" provider to a notifier and have it expose a custom
invalidate
method, then callref.read(aProvider.notifier).invalidate()
– which would then internally callref.invalidate(bProvider)
.
Thanks for the reply and tips, will check your suggestion and get back to you.