-
-
Notifications
You must be signed in to change notification settings - Fork 974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provider still triggers when not watched #3973
Comments
I want to add that using This workaround will make the log out process "work" but the log in process not work since the |
Replace final userID = ref.watch(userProvider).requireValue!.userID;
with final user = await ref.watch(userProvider.future);
final userID = user.userID; |
Hi @snapsl I don't think this is the solution because the null check is where the exception happens. final user = await ref.watch(userProvider.future);
final userID = user.userID; I will get a nullable Ultimately the problem is not the null check. It is that If you add a provider observer, you will see that this provider, gets correctly disposed on log out since there are no watchers, gets updated, gets disposed again. |
This is a much more simplified test, with no AsyncNotifiers. Just a screen where favorites are shown and a logged out screen. import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
final showFavoritesProvider = NotifierProvider<ShowFavoritesNotifier, bool>(
ShowFavoritesNotifier.new,
);
class ShowFavoritesNotifier extends Notifier<bool> {
@override
bool build() => true;
void dontShow() => state = false;
}
final favoritesProvider =
NotifierProvider.autoDispose<UserFavoritesNotifier, List<String>>(
UserFavoritesNotifier.new,
);
class UserFavoritesNotifier extends AutoDisposeNotifier<List<String>> {
@override
List<String> build() {
final showFavorites = ref.watch(showFavoritesProvider);
if (!showFavorites) throw Exception("MUST NEVER HAPPEN");
return ["favorite 1", "favorite 2", "favorite 3"];
}
}
class TestApp extends ConsumerWidget {
const TestApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showFavorites = ref.watch(showFavoritesProvider);
if (showFavorites) return const MaterialApp(home: FavoritesView());
return const MaterialApp(home: LoggedOutView());
}
}
class FavoritesView extends ConsumerWidget {
const FavoritesView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favorites = ref.watch(favoritesProvider);
return Column(
children: [
...favorites.map((e) => Text(e)),
IconButton(
onPressed: () => ref.read(showFavoritesProvider.notifier).dontShow(),
icon: const Icon(Icons.logout),
),
],
);
}
}
class LoggedOutView extends StatelessWidget {
const LoggedOutView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text("LoggedOutView"));
}
}
void main() {
testWidgets('Log out/Log in test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const ProviderScope(child: TestApp()));
// Verify that we're in FavoritesView
expect(find.text('favorite 1'), findsOneWidget);
// Tap the logout icon
await tester.tap(find.byIcon(Icons.logout));
await tester.pumpAndSettle();
// Verify that we're in LoggedOutView
expect(find.text('LoggedOutView'), findsOneWidget);
});
} |
That's not a bug. Riverpod can't know that the listener is going to be removed soon. From Riverpod's PoV, the provider was still listened when it rebuilt it. Overall the pattern you've described isn't one that's recommended. "Eager initialization" is only a widget thing. Providers shouldn't rely on that. They have the tools to Also, providers should be built such that they cancel their pending work upon |
Hi @rrousselGit Although I agree with what you said conceptually, but I think in a real-world complex app, the "should and shouldn't" lines are a bit blurred. For instance, in the dart doc of `watch`://In this situation, what we do not want to do is to sort our list directly inside the build method of our UI, as sorting a list can be //expensive. But maintaining a cache manually is difficult and error prone.
//To solve this problem, we could create a separate [Provider](https://pub.dev/documentation/riverpod/latest/riverpod/Provider-//class.html) that will expose the sorted list, and use [watch]//(https://pub.dev/documentation/riverpod/latest/riverpod/Ref/watch.html) to automatically re-evaluate the list only //when needed.
//In code, this may look like:
final sortProvider = StateProvider((_) => Sort.byName);
final unsortedTodosProvider = StateProvider((_) => <Todo>[]);
final sortedTodosProvider = Provider((ref) {
// listen to both the sort enum and the unfiltered list of todos
final sort = ref.watch(sortProvider);
final todos = ref.watch(unsortedTodosProvider);
// Creates a new sorted list from the combination of the unfiltered
// list and the filter type.
return [...todos].sort((a, b) { ... });
});
//In this code, by using [Provider](https://pub.dev/documentation/riverpod/latest/riverpod/Provider-class.html) + [watch]//(https://pub.dev/documentation/riverpod/latest/riverpod/Ref/watch.html):
//if either sortProvider or unsortedTodosProvider changes, then sortedTodosProvider will automatically be recomputed.
//if multiple widgets depends on sortedTodosProvider the list will be sorted only once.
//if nothing is listening to sortedTodosProvider, then no sort is performed. Especially in the last point:
Of course that makes total sense, but the problem is, the provider got disposed before rebuilding again because of an internal dependency on another provider, not because it was listened to. ref
..onAddListener(() => print("onAddListener"))
..onRemoveListener(() => print("onRemoveListener"))
..onResume(() => print("onResume"))
..onCancel(() => print("onCancel"))
..onDispose(() => print("onDispose")); This is the updated test codeimport 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
final showFavoritesProvider = NotifierProvider<ShowFavoritesNotifier, bool>(
ShowFavoritesNotifier.new,
);
class ShowFavoritesNotifier extends Notifier<bool> {
@override
bool build() => true;
void dontShow() => state = false;
}
final favoritesProvider =
NotifierProvider.autoDispose<UserFavoritesNotifier, List<String>>(
UserFavoritesNotifier.new,
);
class UserFavoritesNotifier extends AutoDisposeNotifier<List<String>> {
@override
List<String> build() {
ref
..onAddListener(() => print("onAddListener"))
..onRemoveListener(() => print("onRemoveListener"))
..onResume(() => print("onResume"))
..onCancel(() => print("onCancel"))
..onDispose(() => print("onDispose"));
final showFavorites = ref.watch(showFavoritesProvider);
if (!showFavorites) throw Exception("MUST NEVER HAPPEN");
return ["favorite 1", "favorite 2", "favorite 3"];
}
}
class TestApp extends ConsumerWidget {
const TestApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showFavorites = ref.watch(showFavoritesProvider);
if (showFavorites) {
return const MaterialApp(home: FavoritesView());
}
return const MaterialApp(home: LoggedOutView());
}
}
class FavoritesView extends ConsumerWidget {
const FavoritesView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favorites = ref.watch(favoritesProvider);
return Column(
children: [
...favorites.map((e) => Text(e)),
IconButton(
onPressed: () => ref.read(showFavoritesProvider.notifier).dontShow(),
icon: const Icon(Icons.logout),
),
],
);
}
}
class LoggedOutView extends StatelessWidget {
const LoggedOutView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text("LoggedOutView"));
}
}
void main() {
testWidgets('Log out/Log in test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const ProviderScope(child: TestApp()));
// Verify that we're in FavoritesView
expect(find.text('favorite 1'), findsOneWidget);
// Tap the logout icon
await tester.tap(find.byIcon(Icons.logout));
await tester.pumpAndSettle();
// Verify that we're in LoggedOutView
expect(find.text('LoggedOutView'), findsOneWidget);
});
} The console will show this order:
This is where the confusion is. Why Finally,
I agree with you but again, in a complex app, assumptions are made. For example, if the user got to a screen where this screen is only showed when a user is logged in, an assumption is made that I apologize for the long comment. I love riverpod and I've been using it for many years now. It's used in my production apps with thousands of users and businesses and I can't thank you enough for creating and maintaining it. |
The provider rebuilt before the view got disposed. Heavy computation can be cancelled if need be. You can override
The fact is, you made an assumption that was incorrect due to how life-cycles work. Rather than throwing/returning null, you could |
Describe the bug
In a common login/logout scenario where data is fetched if there is a user and a logged out screen is shown where there's not, I found a bug where the state of a provider is AsyncError for a moment even though its not watched anymore in the widget tree.
Basically the widget tree is as follows:
MainScreen
watchesuserProvider<User?>
to eagerly initialize it. When complete, we show theHomeScreen
.HomeScreen
based on the state ofuserProvider
we either show aFavoritesScreen
or aLoggedOutScreen
FavoritesScreen
, we watchfavoritesProvider
and fetch a list of favorites. When logging out, we clearuserProvider
HomeScreen
returnsLoggedOutScreen
.favoritesProvider
is no longer watched.I've written a test to verify this behavior.
Moreover, if we add an observer to the container, or
ref.onDispose
in thefavoritesProvider
we can see that the provider is disposed, then updated, then disposed again.To Reproduce
Expected behavior
I expect when I stop watching a provider in the widgets should dispose it immediately
The text was updated successfully, but these errors were encountered: