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 an asynchronous StateNotifier #784

Closed
ryicoh opened this issue Oct 3, 2021 · 9 comments
Closed

Add an asynchronous StateNotifier #784

ryicoh opened this issue Oct 3, 2021 · 9 comments
Labels
enhancement New feature or request

Comments

@ryicoh
Copy link

ryicoh commented Oct 3, 2021

Is your feature request related to a problem? Please describe.
It's related to this Issue. #57, #329

#57 (comment)

class Whatever extends StateNotifier<AsyncValue<int>> {
  Whatever(): super(const AsyncValue.loading()) {
    _fetch();
  }

  Future<void> _fetch() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() => fetch());
  }
}

This solution is good. However, other providers do not allow synchronous calls.

// Good within the widget.
Widget build(context, ref) {
  final w = ref.watch(whatever);
  ...
}

// Other providers can't get initialized objects synchronously.
final other = FutureProvider((ref) async {
  final w = await ref.watch(whatever.future);
                                     _______
  ...
});

Describe the solution you'd like

I made the draft here.
https://github.com/ryicoh/future_state_notifier/blob/85b65cc7c2f96887dcb1442df5945831464623a0/lib/future_state_notifier.dart#L6

abstract class FutureStateNotifier<T> extends StateNotifier<AsyncValue<T>> {
  final lock = Lock();
  final once = SyncOnce();

  FutureStateNotifier() : super(const AsyncValue.loading());

  Future<void> initState();

  Future<void> initStateOnce() async {
    await once(initState);
  }

  Future<void> setState(Future<T> Function() future) async {
    lock.synchronized(() async {
      if (state != const AsyncValue.loading()) {
        state = const AsyncValue.loading();
      }
      state = await AsyncValue.guard(future);
    });
  }
}

Example of Counter
https://github.com/ryicoh/future_state_notifier/blob/85b65cc7c2f96887dcb1442df5945831464623a0/test/future_state_notifier_test.dart#L33

If this provider is to be used within another provider:
https://github.com/ryicoh/future_state_notifier/blob/85b65cc7c2f96887dcb1442df5945831464623a0/lib/future_state_notifier.dart#L33

If this provider is to be used within a flutter widget:
https://github.com/ryicoh/future_state_notifier/blob/85b65cc7c2f96887dcb1442df5945831464623a0/lib/future_state_notifier.dart#L38

I know that the combination of "StateNotifier" and "FutureNotifier" does not result in "Async.loading()" when the state is updated.
https://github.com/ryicoh/future_state_notifier/blob/85b65cc7c2f96887dcb1442df5945831464623a0/test/future_state_notifier_test.dart#L87

@ryicoh ryicoh added enhancement New feature or request needs triage labels Oct 3, 2021
@rrousselGit
Copy link
Owner

Closing since this is supported directly by StateNotifierProvider now thanks to extension methods.

So creating a StateNotifier<AsyncValue<...>> will give access to .future/.stream

@maks
Copy link

maks commented Nov 16, 2021

@rrousselGit I think maybe I'm missing something here, I see how to use AsyncValue to wrap state to a StateNotifier, but what if I have a StateNotifierProvider that depends on another Provider that is a FutureProvider, eg.

final authServiceProvider = FutureProvider<AuthService>((ref) async {

...

final settingsViewModelProvider = StateNotifierProvider<SettingsViewModel, SettingsState>((ref) {
  final auth = await ref.watch(authServiceProvider);
  return SettingsViewModel(authService: auth, state: const SettingsState());
});

How can I do the above as I'd need to make the settingsViewModelProvider in order to be able to make the closure async in order to await on my watching the authServiceProvider?

@maks
Copy link

maks commented Nov 16, 2021

Actually I've ended up just doing this:

final settingsViewModelProvider = StateNotifierProvider<SettingsViewModel, SettingsState?>((ref) {
  final auth = ref.watch(authServiceProvider);
  return SettingsViewModel(authService: auth.value, state: const SettingsState(wifiOnly: true));
});

and making my SettingsViewModel accept a nullable authService but I'm not sure if I'm doing something not quite right here.

@maks
Copy link

maks commented Nov 16, 2021

btw I have looked at #57 and #127 but I don't think they cover quite the same use case, as to me they seem to be about the StateNotifiers State being a AsyncValue whereas here, I have another provider as dependency and it happens to be a FutureProvider.

@om-ha
Copy link

om-ha commented Aug 1, 2022

final settingsViewModelProvider = StateNotifierProvider<SettingsViewModel, SettingsState?>((ref) {
  final auth = ref.watch(authServiceProvider);
  return SettingsViewModel(authService: auth.value, state: const SettingsState(wifiOnly: true));
});

I agree with @maks here, and my use case is exactly like the above

@om-ha
Copy link

om-ha commented Aug 1, 2022

Closing since this is supported directly by StateNotifierProvider now thanks to extension methods.

So creating a StateNotifier<AsyncValue<...>> will give access to .future/.stream

@rrousselGit can you please point us where those extension methods are located? preferably an example usage as well.

Looking for the extension, I searched repo both your repos for on StateNotifierProvider and on StateNotifier to find these extensions, but to no avail.

I need something like FutureProvider + StateNotifierProvider -- more like a FutureStateNotifierProvider.

@om-ha
Copy link

om-ha commented Aug 4, 2022

Actually, I just found a possible answer hidden in a changelog:

https://github.com/rrousselGit/riverpod/blob/master/packages/riverpod/CHANGELOG.md#general-changes

It is now possible to "await" all providers that emit an AsyncValue (previously limited to FutureProvider/StreamProvider). This includes cases where a StateNotifierProvider exposes an AsyncValue:

class MyAsyncStateNotifier extends StateNotifier<AsyncValue<MyState>> {
  MyAsyncStateNotifier(): super(AsyncValue.loading()) {
    // TODO fetch some data and update the state when it is obtained
  }
}

final myAsyncStateNotifierProvider = StateNotifierProvider<MyAsyncStateNotifier, AsyncValue<MyState>>((ref) {
  return MyAsyncStateNotifier();
});

final someFutureProvider = FutureProvider((ref) async {
  MyState myState = await ref.watch(myAsyncStateNotifierProvider.future);
});

However, unfortunately I cannot find a way to prematurely enforce an AsyncValue

final myAsyncStateNotifierProvider = StateNotifierProvider<MyAsyncStateNotifier, AsyncValue<MyState>>((ref) {
  // 1. Fetch/Calculate Result. Some asynchronous Future/AsyncValue goes here

  // 2. Result not ready. I want to return a null `MyAsyncStateNotifier?` or AsyncLoading here but can't!

  // 3. Check if result ready. Future/AsyncValue finished their work.

  // 4. Result ready. Return `MyAsyncStateNotifier`
  return MyAsyncStateNotifier();
});

I cannot seem to achieve the above flow 1. -> 4.

@gmkado
Copy link

gmkado commented Nov 17, 2022

Did this feature go away? I'm on hooks_riverpod: ^2.1.1 and get this error:

The getter 'future' isn't defined for the type 'StateNotifierProvider<MyAsyncStateNotifier, AsyncValue>'.

@rrousselGit
Copy link
Owner

Yes, it never reached the stable release

Use AsyncNotifier instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants