-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Closed
Labels
questionFurther information is requestedFurther information is requested
Description
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:
MainScreenwatchesuserProvider<User?>to eagerly initialize it. When complete, we show theHomeScreen.HomeScreenbased on the state ofuserProviderwe either show aFavoritesScreenor aLoggedOutScreen- In the
FavoritesScreen, we watchfavoritesProviderand fetch a list of favorites. When logging out, we clearuserProvider - When cleared,
HomeScreenreturnsLoggedOutScreen.favoritesProvideris no longer watched. - This is where the error happen, favoritesProvider is "retriggered" and since there is no user, an exception is thrown
I've written a test to verify this behavior.
Moreover, if we add an observer to the container, or ref.onDispose in the favoritesProvider we can see that the provider is disposed, then updated, then disposed again.
To Reproduce
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
final authProvider = AsyncNotifierProvider<AuthNotifier, bool>(
AuthNotifier.new,
);
class AuthNotifier extends AsyncNotifier<bool> {
@override
FutureOr<bool> build() async {
await Future.delayed(const Duration(seconds: 1));
return true;
}
Future<void> logOut() async {
await Future.delayed(const Duration(seconds: 1));
ref.read(userProvider.notifier).logOut();
state = const AsyncData(false);
}
}
typedef User = ({String userID, String userName});
final userProvider = AsyncNotifierProvider<UserNotifier, User?>(
UserNotifier.new,
);
class UserNotifier extends AsyncNotifier<User?> {
@override
FutureOr<User?> build() async {
await Future.delayed(const Duration(seconds: 1));
return (userID: "1", userName: "John Doe");
}
void logOut() {
state = const AsyncData(null);
}
}
final favoritesProvider =
AsyncNotifierProvider.autoDispose<UserFavoritesNotifier, List<String>>(
UserFavoritesNotifier.new,
);
class UserFavoritesNotifier extends AutoDisposeAsyncNotifier<List<String>> {
@override
FutureOr<List<String>> build() async {
//requireValue is used since this provider is eagerly initialized
//This is where the error comes from, null check used on a null value.
//But it should not happen since favoritesProvider is no longer watched
final userID = ref.watch(userProvider).requireValue!.userID;
//Use userID to fetch user's favorites
await Future.delayed(const Duration(seconds: 1));
return ["favorite 1", "favorite 2", "favorite 3"];
}
}
class TestApp extends ConsumerWidget {
const TestApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncUser = ref.watch(userProvider);
return switch (asyncUser) {
AsyncData<User?>() => const MaterialApp(home: Home()),
AsyncError() => const MaterialApp(home: Text("ERROR")),
_ => const MaterialApp(home: CircularProgressIndicator()),
};
}
}
class Home extends ConsumerWidget {
const Home({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isGuestMode = ref.watch(userProvider).requireValue == null;
if (isGuestMode) return const LoggedOutView();
return const LoggedInView();
}
}
class LoggedInView extends ConsumerWidget {
const LoggedInView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncFavorites = ref.watch(favoritesProvider);
return switch (asyncFavorites) {
AsyncData(:final value) => Column(
children: [
...value.map((e) => Text(e)),
IconButton(
onPressed: () async {
await ref.read(authProvider.notifier).logOut();
},
icon: const Icon(Icons.logout),
),
],
),
AsyncError() => const Text("ERROR"),
_ => const CircularProgressIndicator(),
};
}
}
class LoggedOutView extends StatelessWidget {
const LoggedOutView({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text("Logged Out"));
}
}
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()));
//Wait for the fetching of user and user favorites
await tester.pumpAndSettle(const Duration(seconds: 2));
final element = tester.element(find.byType(TestApp));
ProviderScope.containerOf(element).listen(favoritesProvider, (prev, next) {
if (next is AsyncError) {
print("Error: ${next.error}, ${next.stackTrace}");
throw Exception("Should not happen");
}
});
// Verify that there are favorites
expect(find.text('favorite 1'), findsOneWidget);
// Tap the logout icon
await tester.tap(find.byIcon(Icons.logout));
//Wait for the log out to complete
await tester.pumpAndSettle(const Duration(seconds: 1));
// Verify that we're logged out.
expect(find.text('Logged Out'), findsOneWidget);
});
}Expected behavior
I expect when I stop watching a provider in the widgets should dispose it immediately
Metadata
Metadata
Assignees
Labels
questionFurther information is requestedFurther information is requested