diff --git a/packages/flutter_riverpod/lib/src/consumer.dart b/packages/flutter_riverpod/lib/src/consumer.dart index cc5e97037..4a246ddd4 100644 --- a/packages/flutter_riverpod/lib/src/consumer.dart +++ b/packages/flutter_riverpod/lib/src/consumer.dart @@ -20,6 +20,39 @@ abstract class WidgetRef { /// - [listen], to react to changes on a provider, such as for showing modals. T watch(ProviderListenable provider); + /// Determines whether a provider is initialized or not. + /// + /// Writing logic that conditionally depends on the existence of a provider + /// is generally unsafe and should be avoided. + /// The problem is that once the provider gets initialized, logic that + /// depends on the existence or not of a provider won't be rerun; possibly + /// causing your state to get out of date. + /// + /// But it can be useful in some cases, such as to avoid re-fetching an + /// object if a different network request already obtained it: + /// + /// ```dart + /// final fetchItemList = FutureProvider>(...); + /// + /// final fetchItem = FutureProvider.autoDispose.family((ref, id) async { + /// if (ref.exists(fetchItemList)) { + /// // If `fetchItemList` is initialized, we look into its state + /// // and return the already obtained item. + /// final itemFromItemList = ref.watch( + /// fetchItemList.selectAsync((items) => items.firstWhereOrNull((item) => item.id == id)), + /// ); + /// if (itemFromItemList != null) return itemFromItemList; + /// } + /// + /// // If `fetchItemList` is not initialized, perform a network request for + /// // "id" separately + /// + /// final json = await http.get('api/items/$id'); + /// return Item.fromJson(json); + /// }); + /// ``` + bool exists(ProviderBase provider); + /// Listen to a provider and call `listener` whenever its value changes, /// without having to take care of removing the listener. /// @@ -564,6 +597,11 @@ class ConsumerStatefulElement extends StatefulElement implements WidgetRef { _listeners.add(sub); } + @override + bool exists(ProviderBase provider) { + return ProviderScope.containerOf(this, listen: false).exists(provider); + } + @override T read(ProviderListenable provider) { return ProviderScope.containerOf(this, listen: false).read(provider); diff --git a/packages/flutter_riverpod/test/consumer_test.dart b/packages/flutter_riverpod/test/consumer_test.dart index ead4488af..f4bc9c6d9 100644 --- a/packages/flutter_riverpod/test/consumer_test.dart +++ b/packages/flutter_riverpod/test/consumer_test.dart @@ -6,6 +6,31 @@ import 'package:flutter_test/flutter_test.dart'; import 'utils.dart'; void main() { + group('WidgetRef.exists', () { + testWidgets('simple use-case', (tester) async { + late WidgetRef ref; + await tester.pumpWidget( + ProviderScope( + child: Consumer( + builder: (context, r, child) { + ref = r; + return Container(); + }, + ), + ), + ); + + final provider = Provider((ref) => 0); + + expect(ref.exists(provider), false); + expect(ref.exists(provider), false); + + ref.read(provider); + + expect(ref.exists(provider), true); + }); + }); + testWidgets('WidgetRef.context exposes the BuildContext', (tester) async { late WidgetRef ref; diff --git a/packages/riverpod/CHANGELOG.md b/packages/riverpod/CHANGELOG.md index 149962c8d..022d7dde8 100644 --- a/packages/riverpod/CHANGELOG.md +++ b/packages/riverpod/CHANGELOG.md @@ -7,6 +7,7 @@ - Added `Ref.notifyListeners()` to forcibly notify dependents. This can be useful for mutable state. - Added `@useResult` to `Ref.refresh`/`WidgetRef.refresh` +- Added `Ref.exists` to check whether a provider is initialized or not. - `FutureProvider`, `StreamProvider` and `AsyncNotifierProvider` now preserve the previous data/error when going back to loading. This is done by allowing `AsyncLoading` to optionally contain a value/error. diff --git a/packages/riverpod/lib/src/framework/container.dart b/packages/riverpod/lib/src/framework/container.dart index 2649f5476..700ebc8e2 100644 --- a/packages/riverpod/lib/src/framework/container.dart +++ b/packages/riverpod/lib/src/framework/container.dart @@ -251,6 +251,13 @@ class ProviderContainer implements Node { return provider.read(this); } + /// {@macro riverpod.exists} + bool exists(ProviderBase provider) { + final element = _stateReaders[provider]?._element; + + return element != null; + } + /// Executes [ProviderElementBase.debugReassemble] on all the providers. void debugReassemble() { // TODO hot-reload handle provider type change diff --git a/packages/riverpod/lib/src/framework/element.dart b/packages/riverpod/lib/src/framework/element.dart index 4953784cc..75caec33e 100644 --- a/packages/riverpod/lib/src/framework/element.dart +++ b/packages/riverpod/lib/src/framework/element.dart @@ -682,6 +682,9 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu return _container.read(provider); } + @override + bool exists(ProviderBase provider) => _container.exists(provider); + @override T watch(ProviderListenable listenable) { _assertNotOutdated(); diff --git a/packages/riverpod/lib/src/framework/ref.dart b/packages/riverpod/lib/src/framework/ref.dart index 5e68e71a9..0aac0484e 100644 --- a/packages/riverpod/lib/src/framework/ref.dart +++ b/packages/riverpod/lib/src/framework/ref.dart @@ -191,6 +191,41 @@ abstract class Ref { /// safer to use. T read(ProviderListenable provider); + /// {@template riverpod.exists} + /// Determines whether a provider is initialized or not. + /// + /// Writing logic that conditionally depends on the existence of a provider + /// is generally unsafe and should be avoided. + /// The problem is that once the provider gets initialized, logic that + /// depends on the existence or not of a provider won't be rerun; possibly + /// causing your state to get out of date. + /// + /// But it can be useful in some cases, such as to avoid re-fetching an + /// object if a different network request already obtained it: + /// + /// ```dart + /// final fetchItemList = FutureProvider>(...); + /// + /// final fetchItem = FutureProvider.autoDispose.family((ref, id) async { + /// if (ref.exists(fetchItemList)) { + /// // If `fetchItemList` is initialized, we look into its state + /// // and return the already obtained item. + /// final itemFromItemList = ref.watch( + /// fetchItemList.selectAsync((items) => items.firstWhereOrNull((item) => item.id == id)), + /// ); + /// if (itemFromItemList != null) return itemFromItemList; + /// } + /// + /// // If `fetchItemList` is not initialized, perform a network request for + /// // "id" separately + /// + /// final json = await http.get('api/items/$id'); + /// return Item.fromJson(json); + /// }); + /// ``` + /// {@endtemplate} + bool exists(ProviderBase provider); + /// Obtains the state of a provider and causes the state to be re-evaluated /// when that provider emits a new value. /// diff --git a/packages/riverpod/test/framework/provider_container_test.dart b/packages/riverpod/test/framework/provider_container_test.dart index f602e8edb..5c076de27 100644 --- a/packages/riverpod/test/framework/provider_container_test.dart +++ b/packages/riverpod/test/framework/provider_container_test.dart @@ -16,6 +16,86 @@ void main() { child.dispose(); }); + group('exists', () { + test('simple use-case', () { + final container = createContainer(); + final provider = Provider((ref) => 0); + + expect(container.exists(provider), false); + expect(container.getAllProviderElements(), isEmpty); + + container.read(provider); + + expect(container.exists(provider), true); + }); + + test('handles autoDispose', () async { + final provider = Provider.autoDispose((ref) => 0); + final container = createContainer( + overrides: [ + provider.overrideWith((ref) => 42), + ], + ); + + expect(container.exists(provider), false); + expect(container.getAllProviderElements(), isEmpty); + + container.read(provider); + + expect(container.exists(provider), true); + + await container.pump(); + + expect(container.getAllProviderElements(), isEmpty); + expect(container.exists(provider), false); + expect(container.getAllProviderElements(), isEmpty); + }); + + test('Handles uninitialized overrideWith', () { + final provider = Provider((ref) => 0); + final container = createContainer( + overrides: [ + provider.overrideWith((ref) => 42), + ], + ); + + expect(container.exists(provider), false); + expect(container.getAllProviderElements(), isEmpty); + + container.read(provider); + + expect(container.exists(provider), true); + }); + + test('handles nested providers', () { + final provider = Provider((ref) => 0); + final provider2 = Provider((ref) => 0); + final root = createContainer(); + final container = createContainer(parent: root, overrides: [provider2]); + + expect(container.exists(provider), false); + expect(container.exists(provider2), false); + expect(container.getAllProviderElements(), isEmpty); + expect(root.getAllProviderElements(), isEmpty); + + container.read(provider); + + expect(container.exists(provider), true); + expect(container.exists(provider2), false); + expect(container.getAllProviderElements(), isEmpty); + expect(root.getAllProviderElements().map((e) => e.origin), [provider]); + + container.read(provider2); + + expect(container.exists(provider2), true); + expect( + container.getAllProviderElements().map((e) => e.origin), + [provider2], + ); + expect(root.getAllProviderElements().map((e) => e.origin), [provider]); + }); + }); + group('debugReassemble', () { test( 'reload providers if the debugGetCreateSourceHash of a provider returns a different value', diff --git a/packages/riverpod/test/framework/provider_element_test.dart b/packages/riverpod/test/framework/provider_element_test.dart index 9e487774b..b18bb9f37 100644 --- a/packages/riverpod/test/framework/provider_element_test.dart +++ b/packages/riverpod/test/framework/provider_element_test.dart @@ -7,6 +7,28 @@ import 'package:test/test.dart'; import '../utils.dart'; void main() { + group('Ref.exists', () { + test('simple use-case', () { + final container = createContainer(); + final provider = Provider((ref) => 0); + final refProvider = Provider((ref) => ref); + + final ref = container.read(refProvider); + + expect( + container.getAllProviderElements().map((e) => e.origin), + [refProvider], + ); + expect(container.exists(refProvider), true); + expect(ref.exists(provider), false); + + ref.read(provider); + + expect(ref.exists(refProvider), true); + expect(ref.exists(provider), true); + }); + }); + group('ref.notifyListeners', () { test('If called after initialization, notify listeners', () { final observer = ProviderObserverMock();