riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

Delay a FutureProvider until a click is performed

Open edmbn opened this issue 5 years ago • 20 comments

I have a question and I couldn't get an answer on official documentation.

I want to manage the logic of the AsyncValue returned by a FutureProvider in the onPress callback of a button. For example:

signInResult.when(  
   data: (_) {
      // Navigate to a new page
   },
   loading: () {},
   error: (error, stack) {
     // Show Flushbar
   },
);

The problem is that if declare:

final signInResult = useProvider(signInProvider);

When I enter the sign in page for the first time automatically makes the API Request holded by the futureProvider.

Is there any way to use this futureProvider AsyncValue result but not making the request of the future until onPress callback?

Thank you all for your time.

edmbn avatar Sep 23 '20 20:09 edmbn

FutureProvider may not be what you want to use then. You may want a StateProvider or StateNotifierProvider

rrousselGit avatar Sep 24 '20 09:09 rrousselGit

That's what I thought but just to make sure everything is clear: the idea is to listen to this changes on a page in order to show show an alertDialog, for example, based on the asyncValue.error and in case of AsyncValue.data navigate to home page.

Is there any performance issues or excessive rebuilds on having a ProviderListener listening to this stateNotifierProvider like so:

ProviderListener{
    provider: signInResultProvider.state, 
    onChange: (value){}
}

alongside with a

final signInResult = useProvider(sigInResultProvider.state)

to be able to access this state to change widgets:

signinResult.when(
    data: (data){},
    loading: (){},
    error: (err, stack){},
)

Will listening to this providers on different sites but on the same page be a problem or is there a better way to manage this situations?

edmbn avatar Sep 24 '20 10:09 edmbn

That's fine, I don't see an issue here

Le jeu. 24 sept. 2020 à 12:24, Eduard Monfort [email protected] a écrit :

That's what I thought but just to make sure everything is clear: the idea is to listen to this changes on a page in order to show show an alertDialog, for example, based on the asyncValue.error and in case of AsyncValue.data navigate to home page.

Is there any performance issues or excessive rebuilds on having a ProviderListener listening to this stateNotifierProvider like so:

ProviderListener{ provider: signInResultProvider.state, onChange: (value){} }

alongside with a

final signInResult = useProvider(sigInResultProvider.state)

to be able to access this state to change widgets:

signinResult.when( data: (data){}, loading: (){}, error: (err, stack){}, )

Will listening to this providers on different sites but on the same page be a problem or is there a better way to manage this situations?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rrousselGit/river_pod/issues/155#issuecomment-698256959, or unsubscribe https://github.com/notifications/unsubscribe-auth/AEZ3I3N5NVWNW2ZPG7KTAMLSHMM4LANCNFSM4RXPUIOQ .

rrousselGit avatar Sep 24 '20 10:09 rrousselGit

That's great. There is no way to have multiple listeners at the same time right? We need to create a ProviderListener as child of a ProviderListener in order to have two at the same time?

edmbn avatar Sep 24 '20 10:09 edmbn

Indeed

rrousselGit avatar Sep 24 '20 10:09 rrousselGit

Sorry for starting a conversation again but I think could be an interesting discussion. I see lots of cases where you want to watch for a futureProvider changes but only after an event, onPress button for example. It's true that you can create a stateNotifierProvider that updates it's state with an asyncValue but I think most of times isn't necessary a full state notifier with a single function just to get all the updates of a futureProvider result (AsyncValue.loading, AsyncValue.data, AsyncValue.error).

What do you all think of having a modifier, like a .lazy that only creates a futureProvider for example once an event is triggered. This way we would be able to have widgets prepared for listening for changes in this FutureProvider but not triggering the future on useProvider declaration.

edmbn avatar Sep 28 '20 13:09 edmbn

Do you have a concrete use-case example?

rrousselGit avatar Sep 28 '20 14:09 rrousselGit

Yes. For example I want to retrieve user information and here with a futureProvider like we are using it right now it's perfect. But now I want to update this user information. I want to have a circularProgressIndicator() as a child of a button when the "update" button is pressed and also, when the future throws an error I want to show an alert and when data arrived from future I want to navigate. In this case if I declare:

final updateUser = useProvider(updateUserProvider);

the future will be triggered just entering the page. I want to listen to changes whenever I press update button in order to manage the events and also be able to have widgets listening to changes that must be done after onPress:

RaisedButton(
    onPress: {
        updateUser.when(  
           data: (_) {
              // Navigate to a new page
           },
           loading: () {},
           error: (error, stack) {
             // Show Flushbar
           },
        );
    },
    child: updateUser.when(  
           data: (_) {
              return Text('Update'),
           },
           loading: () => CircularProgressIndicator(),
           error: (error, stack) {
             return Text('Try again'),
           },
        );
),

edmbn avatar Sep 28 '20 14:09 edmbn

Is the example I provided clear enough? If anyone has good ideas about this I think could be very beneficial for the package. It's a case we can see quite very often (sign in, sign up, update already retrieved information...)

edmbn avatar Sep 30 '20 15:09 edmbn

You could have two providers:

final event = Provider((ref) => StreamController<Event>());

final future = FutureProvider((ref) async {
  await ref.watch(event).first;
  print('do something');
})


onPressed: () => context.read(event).add(Event()),

rrousselGit avatar Sep 30 '20 16:09 rrousselGit

Yes, but seems just like a workaround, just like create a stateNotifier with only one function just to achieve this. Even more, there could be the case where you don't want the presentation layer to know which event must be called, the button just wants to access to wherever this provider returns. What do you think?

edmbn avatar Sep 30 '20 19:09 edmbn

Riverpod can't reasonably include a modifier for every single use-case possible

Unless it is proven that this is a use-case that everybody wants, the proposed solutions are good enough imo

rrousselGit avatar Sep 30 '20 19:09 rrousselGit

Of course!! Just wanted to start this discussion to allow other people read and talk about this kind of cases for the good of the package.

Thank you @rrousselGit

edmbn avatar Sep 30 '20 20:09 edmbn

I will add my 3 cents here. What I'm currently doing is I have RequestStateNotifier. Which is almost the same as the FutureProvider with AsyncValue but it also has Idle state when action was not performed yet.

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

part 'request_state_notifier.freezed.dart';

abstract class RequestStateNotifier<T> extends StateNotifier<RequestState<T>> {
  RequestStateNotifier() : super(RequestState.idle());

  //It returns a Future with state if you want to avoid ProviderListener
  Future<RequestState<T>> makeRequest(Future<T> Function() function) async {
    try {
      state = RequestState<T>.loading();
      final response = await function();
      final newState = RequestState<T>.success(response);
      if (mounted) {
        state = newState;
      }
      return newState;
    } catch (e, st) {
      final newState = RequestState<T>.error(e, st);
      if (mounted) {
        state = newState;
      }
      return newState;
    }
  }
}

@freezed
abstract class RequestState<T> with _$RequestState<T> {
  const factory RequestState.idle() = Idle<T>;

  const factory RequestState.loading() = Loading<T>;

  const factory RequestState.success(@nullable T value) = Success<T>;

  const factory RequestState.error(Object error, [StackTrace stackTrace]) =
      Error<T>;
}

Which I override for a page when I just want to do some request:

final signInEmailRequestProvider =
    StateNotifierProvider<SignInEmailRequestNotifier>(
  (ref) => SignInEmailRequestNotifier(ref.watch(apiProvider)),
);

class SignInEmailRequestNotifier extends RequestStateNotifier<void> {
  final NetworkApi _api;

  SignInEmailRequestNotifier(this._api);

  Future<void> signIn(String email) => makeRequest(() => _api.signIn(email));
}

Then I can use signInEmailRequestProvider to call the method and listen to the states using useProvider and/or ProviderListener.

So far it works fine for me if I want to make simple requests without any complex logic that would go to StateNotifier. Maybe someone will find it helpful as well.

MarcinusX avatar Feb 18 '21 06:02 MarcinusX

@rrousselGit

just informing, i also have got this usecase.

PavanGangireddy avatar Dec 27 '21 19:12 PavanGangireddy

Hey @MarcinusX , I was searching for a solution for an issue related to a similar implementation. Do you have an example or article about then using signInEmailRequestProvider, as useProvider and not available now ?

zaprogrammer avatar Jan 28 '22 14:01 zaprogrammer

I don't understand. Now you'd use ref.watch and use as a typical StateNotifierProvider. Oh, also add RequestStateNotifier<void> to Provider Type, so it's StateNotifierProvider<SignInEmailRequestNotifier, RequestStateNotifier<void>>(...

MarcinusX avatar Jan 29 '22 08:01 MarcinusX

I think this question can be resolved with a nullable, then the initial state will be AsyncData with a null value.

You can too use another class that encapsulates that value.

kmartins avatar Feb 23 '22 16:02 kmartins

nullable AsyncValue can't be used with StateNotifier (#1282)

If anyone want to implement this feature with AsyncValue, consider this solution. This allows you to take advantage of AsyncValue features.

abstract class LazyFutureNotifier<T> extends StateNotifier<LazyFutureState<T>> {
  LazyFutureNotifier(LazyFutureState<T> state) : super(state);

  late final AsyncLoading<T> _loading;

  @protected
  Future<void> makeRequest(Future<T> Function() callback) async {
    if (state.isWaiting) state = state.copyWith(isWaiting: false);

    if (state.value is AsyncLoading) {
      _loading = state.value as AsyncLoading<T>;
    } else {
      state = state.copyWith(value: _loading.copyWithPrevious(state.value));
    }

    final result = await AsyncValue.guard(callback);
    if (mounted) state = state.copyWith(value: result);
  }
}

@freezed
class LazyFutureState<T> with _$LazyFutureState<T> {
  const factory LazyFutureState({
    @Default(true) bool isWaiting,
    required AsyncValue<T> value,
  }) = _LazyFutureState<T>;
}

appano1 avatar Mar 22 '22 04:03 appano1

Yes, however, the null is in the value of the AsynValue.

class MyNotifier extends StateNotifier<AsyncValue<MyData?>> {
  MyNotifier() : super(const AsyncValue.data(null));

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

You yet can encapsulate the value in a Unions/Sealed class with freezed.

kmartins avatar Mar 22 '22 13:03 kmartins

THis probably can be closed in favor of https://github.com/rrousselGit/riverpod/issues/1660

rrousselGit avatar Sep 21 '22 21:09 rrousselGit