Skip to content

Commit

Permalink
Add Ref.exists (#1844)
Browse files Browse the repository at this point in the history
  • Loading branch information
rrousselGit committed Oct 30, 2022
1 parent 8b3ab0c commit 3ddbb5c
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 0 deletions.
38 changes: 38 additions & 0 deletions packages/flutter_riverpod/lib/src/consumer.dart
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions packages/flutter_riverpod/test/consumer_test.dart
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions packages/riverpod/CHANGELOG.md
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions packages/riverpod/lib/src/framework/container.dart
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/riverpod/lib/src/framework/element.dart
Expand Up @@ -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();
Expand Down
35 changes: 35 additions & 0 deletions packages/riverpod/lib/src/framework/ref.dart
Expand Up @@ -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.
///
Expand Down
80 changes: 80 additions & 0 deletions packages/riverpod/test/framework/provider_container_test.dart
Expand Up @@ -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',
Expand Down
22 changes: 22 additions & 0 deletions packages/riverpod/test/framework/provider_element_test.dart
Expand Up @@ -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();
Expand Down

0 comments on commit 3ddbb5c

Please sign in to comment.