Repository: rrousselGit/riverpod

Issue:
Add a way to imperatively determine if a provider is initialized or not.
Let's add a `ref.exists(provider)`

Abeilt unsafe, this can be useful to de-duplicate network requests in the event that the data is already obtained in a separate provider – without initializing said provider if it isn't used yet.



Fix patch:
diff --git a/packages/flutter_riverpod/lib/src/consumer.dart b/packages/flutter_riverpod/lib/src/consumer.dart
index cc5e97037f..4a246ddd47 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<T>(ProviderListenable<T> 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<List<Item>>(...);
+  ///
+  /// final fetchItem = FutureProvider.autoDispose.family<Item, String>((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<Object?> 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<Object?> provider) {
+    return ProviderScope.containerOf(this, listen: false).exists(provider);
+  }
+
   @override
   T read<T>(ProviderListenable<T> provider) {
     return ProviderScope.containerOf(this, listen: false).read(provider);
diff --git a/packages/riverpod/CHANGELOG.md b/packages/riverpod/CHANGELOG.md
index 149962c8d6..022d7dde82 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 2649f54769..700ebc8e2d 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<Object?> 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 4953784ccd..75caec33e7 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<Object?> provider) => _container.exists(provider);
+
   @override
   T watch<T>(ProviderListenable<T> listenable) {
     _assertNotOutdated();
diff --git a/packages/riverpod/lib/src/framework/ref.dart b/packages/riverpod/lib/src/framework/ref.dart
index 5e68e71a93..0aac0484e6 100644
--- a/packages/riverpod/lib/src/framework/ref.dart
+++ b/packages/riverpod/lib/src/framework/ref.dart
@@ -191,6 +191,41 @@ abstract class Ref<State extends Object?> {
   /// safer to use.
   T read<T>(ProviderListenable<T> 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<List<Item>>(...);
+  ///
+  /// final fetchItem = FutureProvider.autoDispose.family<Item, String>((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<Object?> provider);
+
   /// Obtains the state of a provider and causes the state to be re-evaluated
   /// when that provider emits a new value.
   ///


Test patch:
diff --git a/packages/flutter_riverpod/test/consumer_test.dart b/packages/flutter_riverpod/test/consumer_test.dart
index ead4488aff..f4bc9c6d99 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/test/framework/provider_container_test.dart b/packages/riverpod/test/framework/provider_container_test.dart
index f602e8edb8..5c076de276 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 9e487774bc..b18bb9f378 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();