Skip to content

Commit

Permalink
Added Notifier.stateOrNull
Browse files Browse the repository at this point in the history
This will return null if used when the notifier is uninitialized or in error state.
  • Loading branch information
rrousselGit committed Aug 30, 2023
1 parent 6fcd488 commit ffaa92c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 11 deletions.
6 changes: 6 additions & 0 deletions packages/riverpod/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased minor

- Added `Notifier.stateOrNull`.
This will return `null` if used when the notifier has yet to be initialized
or is in error state.

## 2.3.10 - 2023-08-28

Riverpod now requires package:meta >=1.9.0
Expand Down
18 changes: 8 additions & 10 deletions packages/riverpod/lib/src/framework/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
///
/// This API is not meant for public consumption. Instead if a [Ref] needs
/// to expose a way to update the state, the practice is to expose a getter/setter.
@protected
@internal
void setState(State newState) {
assert(
() {
Expand All @@ -167,8 +167,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
///
/// This is not meant for public consumption. Instead, public API should use
/// [readSelf].
@protected
@visibleForTesting
@internal
Result<State>? getState() => _state;

/// Read the current value of a provider and:
Expand All @@ -178,7 +177,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
///
/// This is not meant for public consumption. Instead, public API should use
/// [readSelf].
@protected
@internal
State get requireState {
assert(
() {
Expand All @@ -205,7 +204,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {

/// Called when a provider is rebuilt. Used for providers to not notify their
/// listeners if the exposed value did not change.
@visibleForOverriding
@internal
bool updateShouldNotify(State previous, State next);

/* /STATE */
Expand Down Expand Up @@ -233,8 +232,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
}

/// Called the first time a provider is obtained.
@protected
@mustCallSuper
@internal
void mount() {
_mounted = true;
assert(
Expand Down Expand Up @@ -281,8 +279,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
/// See also:
/// - `overrideWithValue`, which relies on [update] to handle
/// the scenario where the value changed.
@protected
@mustCallSuper
@internal
// ignore: use_setters_to_change_properties
void update(ProviderBase<State> newProvider) {
_provider = newProvider;
Expand Down Expand Up @@ -320,6 +317,7 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
///
/// This is not meant for public consumption. Public API should hide
/// [flush] from users, such that they don't need to care about invocing this function.
@internal
void flush() {
_maybeRebuildDependencies();
if (_mustRecomputeState) {
Expand Down Expand Up @@ -402,10 +400,10 @@ abstract class ProviderElementBase<State> implements Ref<State>, Node {
/// - [didChangeDependency] can be used to differentiate a rebuild caused
/// by [watch] from one caused by [refresh]/[invalidate].
@visibleForOverriding
@protected
void create({required bool didChangeDependency});

/// Invokes [create] and handles errors.
@internal
void buildState() {
ProviderElementBase<Object?>? debugPreviouslyBuildingElement;
final previousDidChangeDependency = _didChangeDependency;
Expand Down
20 changes: 19 additions & 1 deletion packages/riverpod/lib/src/notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ abstract class NotifierBase<State> {

/// The value currently exposed by this [Notifier].
///
/// If used inside [Notifier.build], may throw if the notifier is not yet initialized.
///
/// Invoking the setter will notify listeners if [updateShouldNotify] returns true.
/// By default, this will compare the previous and new value using [identical].
///
Expand All @@ -34,10 +36,26 @@ abstract class NotifierBase<State> {
@visibleForTesting
State get state {
_element.flush();
// ignore: invalid_use_of_protected_member
return _element.requireState;
}

/// The value currently exposed by this [Notifier].
///
/// If used inside [Notifier.build], may return null if the notifier is not yet initialized.
/// It will also return null if [Notifier.build] threw.
///
/// Invoking the setter will notify listeners if [updateShouldNotify] returns true.
/// By default, this will compare the previous and new value using [identical].
///
/// Reading [stateOrNull] if the provider is out of date (such as if one of its
/// dependency has changed) will trigger [Notifier.build] to be re-executed.
@protected
@visibleForTesting
State? get stateOrNull {
_element.flush();
return _element.getState()?.stateOrNull;
}

@protected
@visibleForTesting
set state(State value) {
Expand Down
103 changes: 103 additions & 0 deletions packages/riverpod/test/providers/notifier/notifier_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,109 @@ import 'factory.dart';
void main() {
for (final factory in matrix()) {
group(factory.label, () {
group('Notifier.stateOrNull', () {
test('returns null during first build until state= is set', () {
final stateInBuild = <int?>[];

final provider = NotifierProvider<TestNotifier<int>, int>(() {
late TestNotifier<int> notifier;
return notifier = TestNotifier((ref) {
stateInBuild.add(notifier.stateOrNull);
return 0;
});
});
final container = createContainer();

final sub = container.listen(
provider.notifier,
(_, __) {},
);

expect(stateInBuild, [null]);

expect(sub.read().stateOrNull, 0);
});

test('returns null if Notifier.build threw', () {
final provider = factory.simpleTestProvider<int>(
(ref) => throw Exception('42'),
);
final container = createContainer();

final sub = container.listen(
provider.notifier,
(_, __) {},
);

expect(sub.read().stateOrNull, null);
});

test(
'returns the previous state if using inside Notifier.build '
'after the state was already initialized', () {
final stateInBuild = <int?>[];

final provider = NotifierProvider<TestNotifier<int>, int>(() {
late TestNotifier<int> notifier;
return notifier = TestNotifier((ref) {
stateInBuild.add(notifier.stateOrNull);
return 0;
});
});
final container = createContainer();

final sub = container.listen(
provider.notifier,
(_, __) {},
);

sub.read().state = 42;
container.refresh(provider);

expect(stateInBuild, [null, 42]);
});

test('Post build, returns the current state', () {
final provider = factory.simpleTestProvider<int>(
(ref) => 0,
);
final container = createContainer();

final sub = container.listen(
provider.notifier,
(_, __) {},
);

expect(sub.read().stateOrNull, 0);

sub.read().state = 42;

expect(sub.read().stateOrNull, 42);
});

test(
'On invalidated providers, rebuilds the notifier and return the new state',
() {
final provider = factory.simpleTestProvider<int>(
(ref) => 0,
);
final container = createContainer();

final sub = container.listen(
provider.notifier,
(_, __) {},
);

sub.read().state = 42;

expect(sub.read().stateOrNull, 42);

container.invalidate(provider);

expect(sub.read().stateOrNull, 0);
});
});

test(
'uses notifier.build as initial state and update listeners when state changes',
() {
Expand Down

0 comments on commit ffaa92c

Please sign in to comment.