rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
5.82k stars 888 forks source link

Bad state: called ProviderSubscription.read on a subscription that was closed #3486

Closed a1573595 closed 1 month ago

a1573595 commented 1 month ago

Describe the bug Update ProviderSubscription value throw Bad state exception in hooks_riverpod 2.5.1, 2.4.10 is well.

To Reproduce

Code sample ```dart import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; void main() { runApp(const ProviderScope(child: MaterialApp(home: HomePage()))); } final keepAliveLock = StateProvider.autoDispose((ref) => false); final dataSource1 = FutureProvider.autoDispose((ref) { final link = ref.keepAlive(); ref.listen(keepAliveLock, (previous, next) { link.close(); }); return Future.delayed(const Duration(seconds: 3), () => "Data1"); }); final dataSource2 = FutureProvider.autoDispose((ref) { final link = ref.keepAlive(); ref.listen(keepAliveLock, (previous, next) { link.close(); }); return Future.delayed(const Duration(seconds: 3), () => "Data2"); }); final dataSource3 = FutureProvider.autoDispose((ref) { final link = ref.keepAlive(); ref.listen(keepAliveLock, (previous, next) { link.close(); }); return Future.delayed(const Duration(seconds: 3), () => "Data3"); }); class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: TextButton( onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const NextPage())), child: const Text("Next"), ), ), ); } } class NextPage extends HookConsumerWidget { const NextPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final pages = useMemoized(() => const [ FirstPage(), SecondPage(), ThirdPage(), ]); final tabController = useTabController(initialLength: pages.length); useEffect(() { final subscription = ref.listenManual(keepAliveLock.notifier, (prev, next) {}); return () { Future(() { subscription.read().update((state) => !state); subscription.close(); }); }; }, const []); return Scaffold( appBar: AppBar(), body: TabBarView( physics: const BouncingScrollPhysics(), controller: tabController, children: pages, ), ); } } class FirstPage extends ConsumerWidget { const FirstPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Center( child: ref.watch(dataSource1).when( data: (data) => Text(data), error: (error, stackTrace) => Text(error.toString()), loading: () => const CircularProgressIndicator(), ), ); } } class SecondPage extends ConsumerWidget { const SecondPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Center( child: ref.watch(dataSource2).when( data: (data) => Text(data), error: (error, stackTrace) => Text(error.toString()), loading: () => const CircularProgressIndicator(), ), ); } } class ThirdPage extends ConsumerWidget { const ThirdPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Center( child: ref.watch(dataSource3).when( data: (data) => Text(data), error: (error, stackTrace) => Text(error.toString()), loading: () => const CircularProgressIndicator(), ), ); } } ```

Expected behavior No throw exception and release provider when ProviderSubscription close.

rrousselGit commented 1 month ago

This is expected. The ProviderSubscription was already closed my Riverpod by the time you called .read()

Riverpod closes all subscriptions associated to a widget when the widget is unmounted. So you cannot use sub.read() after the widget got unmounted – which is what you do here.

a1573595 commented 1 month ago

This is expected. The ProviderSubscription was already closed my Riverpod by the time you called .read()

Riverpod closes all subscriptions associated to a widget when the widget is unmounted. So you cannot use sub.read() after the widget got unmounted – which is what you do here.

So there is currently no other solution to cache data?

I hope not to load all the DataSource at once when entering NextPage, and release the KeepAliveLink of DataSource after leaving NextPage.

rrousselGit commented 1 month ago

The issue is likely your Future wrapping the .read()

I guess removing it should work

a1573595 commented 1 month ago

The issue is likely your Future wrapping the .read()

I guess removing it should work

I thought so before, but actually it's not.

Removing the future wrapping throws an error:

Having a Future throws an error:

rrousselGit commented 1 month ago

Sounds like you used Future to cheat around the error. That wasn't really supported

Looks like Consumer supports calling sub.read inside ConsumerState.dispose. Sounds like we would want to support this for useEffect. Do you mind opening a feature request for this?