Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ref.exists #1844

Merged
merged 1 commit into from Oct 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "await" is missing here. Without "await" itemFromItemList is always != null because it is a Future<Item?>

/// 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem here, i think "await" is missing

/// 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