Code Monkey home page Code Monkey logo

Comments (43)

rrousselGit avatar rrousselGit commented on May 10, 2024 51
Future<int> fetch() aynsc => 42;

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());
  }
}

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024 21

Yes... But not for long πŸ‘€

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024 5

Well AsyncNotifier is out, although undocumented.
So this problem should be solved

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024 4

The suggestion

Future<int> fetch() aynsc => 42;

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());
  }
}

works, and isn't super complicated, but it would be quite nice if a StateNotifier that receives some async input could be set up to not have to deal with that async input itself.

For instance, let's say we have a

class TodoListManager extends StateNotifier<List<Todo>> {...}

And getting that initial List<Todo> depends on something that is provided asynchronously, let's say we need to initialize a repository (or just fetch items from it asynchronously):

final repositoryProvider = FutureProvider<Repository>((ref) async {
   final database = await ref.watch(databaseProvider.future);
   final repository = getRepository(database);
   return repository;
});

Now, I can have my TodoListManager extends StateNotifier<AsyncValue<List<Todo>>> and have it perform whatever databaseProvider does itself, like the suggestion. However, now my TodoListManager will need to add conditions to all its operations to check if the data has been loaded. Also, if I want to write a test for my TodoListManager, rather than just passing it a List<Todo>, I now have to pass it a (mock) database. It would be a lot easier if I could wrap the whole thing in something that deals with the async loads, and from that point on just have a StateNotifier that works with the loaded data. That way the StateNotifier doesn't need to be explicitly responsible for where that data came from.

I guess a FutureStateNotifierProvider is what I'm looking for, which would return an AsyncValue for the state as well as for the provider.notifier. I have to admit that I don't understand the fundamentals of this all well enough (yet) to know if this is even possible. It's also possible I'm completely missing the point somewhere, in which case I apologize.

For what it's worth, I'm currently working around this issue by creating a helper class with all the logic that I would like to have in my StateNotifier, and using that helper class from the actual StateNotifier<AsyncValue<..>>. This mitigates the testability and state check issues, because I can test the helper class separately, but it feels like hack.

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024 4

I was only advocating this method because you said you wanted to 1) get data from some async function and 2) store that data in a statenotifier without requiring the use of AsyncValue. What I posted would accomplish that, I think. I suggested using initState() of a stateful widget because that method fires only once upon widget creation. It doesn't necessarily have anything to do with that widget's state. You're just putting a side effect in some widget's initState()

Fair enough, it looks like I'm not doing a good enough job to clearly put in to words what I think is causing the confusion. So let me try to paint a scenario.

Let's say I'm working on a TODO list app. I have a page where TODOs are displayed, added and deleted, and a page that displays TODO statistics. Everything is set up using mock data for now. Here's the provider for the statistics page:

final statsProvider = Provider((ref) {
  final todos = ref.watch(mockRepository).getTodos(); 
  return Statistics(todos);
}

ref.watch(statsProvider); // Statistics

For the list/add/edit/delete page I have a TodoManager extends StateNotifier<List<Todo>>. Here's its provider:

final todoManagerProvider = StateNotifierProvider<TodoManager, List<Todo>>((ref) {
 final todos = ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

ref.watch(todoManagerProvider); // List<Todo>
ref.watch(todoManagerProvider.notifier); // TodoManager

The whole UI is set up and working fine, now I'm ready to hook up real data. I come to the conclusion that in order to load the TODOs, I need an async call. So, I swap out the statistics provider for a FutureProvider:

final statsProvider = FutureProvider((ref) {
  final todos = await ref.watch(mockRepository).getTodos(); 
  return Statistics(todos);
}

ref.watch(statsProvider); // AsyncValue<Statistics>

Of course the UI will have to deal with the output being AsyncValue<Statistics>, but I'm managing the dependencies in the same place, and everything else stays roughly the same. Now it's time for the StateNotifier, and here's where I think people get confused. Based on the above, I'd say what people most likely expect is the following:

// THIS DOESN'T WORK
// ...for whoever is just skimming the code blocks
final todoManagerProvider = FutureStateNotifierProvider<TodoManager, List<Todo>>((ref) async {
 final todos = await ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

ref.watch(todoManagerProvider); // AsyncValue<List<Todo>>
ref.watch(todoManagerProvider.notifier); // AsyncValue<TodoManager>

Again the UI will have to deal with those AsyncValues again, but otherwise everything stays the same.

Instead, the solution requires moving all async initialization into the TodoManager, which can quite drastically change how that class behaves internally, because every add / delete / read operation needs to be aware of the loading status of the data. It requires a restructuring of the code on both ends. Now I haven't quite made up my mind on whether or not this is justified (there's maybe something fundamentally different about that last pretend code), but it certainly is unexpected. It may just be a matter of documentation. I'd be happy to help there by the way, I'm already producing pages of prose here anyway πŸ˜›.

All that being said

What I wanted for my actual code was for everything internally to stay the same, and for nested widgets to have easy access to an already loaded StateNotifier through ref.watch(). In my case that StateNotifier is also part of a .family, and I didn't really want to pass either that StateNotifier or the .family parameter that creates it down the tree. It was that last part that made me realize I could use a ProviderScope to solve the issue. Applied to the previous, my solution now is:

final futureTodoManagerProvider = FutureProvider((ref) async {
 final todos = await ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

final todoManagerProvider = StateNotifierProvider<TodoManager, List<Todo>>((ref) => 
   throw UnimplementedError("Not provided"));

Widget someBuildMethod(BuildContext context, ref) {
  final manager = ref.watch(futureTodoManagerProvider);
  return manager.when(
    error: (_, __) => Text('ERROR'),
    loading: () => Text('LOADING'),
    data: (data) => ProviderScope(
      overrides: [todoManagerProvider.overrideWithValue(data)],
      child: Container(/* Everything in here is the same as before */),
    ),
  );
}

This does everything I need it to, including giving me the ability to simply ref.watch(todoManagerProvider) anywhere in the nested widget tree, even when that original TodoManager was part of a family.

from riverpod.

edmbn avatar edmbn commented on May 10, 2024 1

Hello @joanofdart I don't think it is needed any update. It is clear the usage of asyncValue with Remi's example. The use case I exposed later could be solved really easy using a FutureProvider for update logic.

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024 1

@ebelevics Consider callling getBaseData inside your baseDataProvider provider instead of a widget

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024 1

I remember there were mentions about possible undesirable effects of nesting ProviderScope but can't find the discussion at this moment.

@mcrio The primary practical downside I experience is that a provider that uses another provider that is overridden in a ProviderScope needs to explicitly specify its dependencies, or a runtime error will be thrown. Otherwise the solution has been working quite well for me.

...I'm absolutely going to find out what AsyncNotifier is in the next few minutes, though.

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024

Is extending StateNotifier<AsyncValue> what you're looking for?

from riverpod.

edmbn avatar edmbn commented on May 10, 2024

Can you provide a simple example? I am quite confused about how it should be done. Thank you πŸ™πŸ»

from riverpod.

edmbn avatar edmbn commented on May 10, 2024

Oh I see!!! That should do it, yes. That really helped. Thank you so much for everything!

from riverpod.

edmbn avatar edmbn commented on May 10, 2024

But there's a case involved in this situation I cannot see.

We have the todo list fetched and saved correctly in our stateNotifier extending asyncValue. We decide now to edit some todos, calling a update method inside the stateNotifier class and proceed to post this new data to API to save this edited todos in a database but the API call fails to update this todos on server. And now the user, instead of trying again wants to cancel edition and retrieve latest state available.

In this case the provider's state is returning AsyncValue.error but since we canceled edition we want to see latest AsyncValue.data.

How should we proceed in this case? Maybe stateNotifier is not the best suited for this case? But we also want the data, loading, error management.

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024

You could override the setter of state to internally keep track of the latest valid value

from riverpod.

edmbn avatar edmbn commented on May 10, 2024

Yes... But look like a difficult solution to something quite usual. Maybe would be better to create a FutureProvider apart to handle update logic.

from riverpod.

joanofdart avatar joanofdart commented on May 10, 2024

Hello πŸ‘‹ has there been any updates on this : D? just checking in since I'm really digging river_pod but I'm a bit dumb and having a hard time with async data (firebase-stuff).

from riverpod.

tbm98 avatar tbm98 commented on May 10, 2024

For simple

Future<int> fetch() aynsc => 42;

final Whatever = FutureProvider<int> async{

  final result = await fetch();

  return result;
}

and context.refresh(Whatever) if you want to refresh it

from riverpod.

joanofdart avatar joanofdart commented on May 10, 2024

thank you both @tbm98 and @edmbn
May I ask you to take a look at this link? #104 I'm trying to learn riverpod and hopefully I'm going it the right way.

from riverpod.

tbm98 avatar tbm98 commented on May 10, 2024

I see. but in your use-case, I think you don't need to use StateNotifier, use FutureProvider is enough.
in #104 it has more than one behavior.

from riverpod.

cswkim avatar cswkim commented on May 10, 2024
Future<int> fetch() async => 42;

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());
  }
}

If the initialize async data is a custom object like:

Future<MyCustomModel> fetch() async {
  return MyCustomModel(propA: 'hello', propB: 42, propC: true);
}

What's the code look like for a setter method? Is there some sort of copyWith type of syntax to set a single property of the async state?

class Whatever extends StateNotifier<AsyncValue<MyCustomModel>> {
  Future<void> setPropA(String newValue) async {
    // what to do here?
    // state = const AsyncValue.loading() - use this here as well?
  }

  Future<void> setPropB(int newValue) async {}
  Future<void> setPropC(bool newValue) async {}
}

from riverpod.

jlnrrg avatar jlnrrg commented on May 10, 2024

@rrousselGit
I tried the above mentioned approach and locked at the performance analysis.
In both cases (use of Consumer and useProvider) there was a mentionable delay.
For my inexperienced eyes it looks as if the statenotifier blocks the widget build till the fetch method is run through. It would be awesome if you could reconfirm on your side that there is no problem. (best not to use my code as I simplified it quite a bit)

StateNotifier
final stateNotifierProvider =
    StateNotifierProvider.autoDispose(
        (ProviderReference ref) => ReaderStateNotifier());

class ReaderStateNotifier
    extends StateNotifier<AsyncValue<KtList<User>>> {
  ReaderStateNotifier()
      : super(const AsyncValue<KtList<User>>.loading()) {
    fetched();
  }

  Future<void> fetched() async {
    state = const AsyncValue<KtList<User>>.loading();

    final Either<ModelFailure, KtList<User>> result =
        await userRepository.read();

    state = result.fold((ModelFailure l) => AsyncValue<KtList<User>>.error(l),
        (KtList<User> r) => AsyncValue<KtList<User>>.data(r));
  }
}
Widget
Scaffold(
          body: Consumer(builder: (context, watch, _) {
            final state = watch(stateNotifierProvider.state);
            return state.when(
              data: (list) => ListView.builder(
                shrinkWrap: true,
                itemCount: list.size,
                itemBuilder: (BuildContext context, int index) {
                  final User user = list[index];
                    return UserListTile(
                      user: user,
                    );
                },
              ),
              loading: () => const Center(child: CircularProgressIndicator()),
              error: (err, _) => Container(
                  color: Colors.red,
                  child: Text(
                      err.toString())), // TODO(jr): show real error handling)
            );
          }),
        );

image

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024

For my inexperienced eyes it looks as if the statenotifier blocks the widget build till the fetch method is run through.

That isn't the case.

Could you provide a full example of how to reproduce this performance issue?

from riverpod.

jlnrrg avatar jlnrrg commented on May 10, 2024

@rrousselGit sorry that is took a while.
I first had to make sure that the issue is still present with the 0.14 version of riverpod.

To recreate the issue please, I created a separate repo, where you should be able to reproduce the error.

Furthermore here are the specific steps I took.

  1. restart the app (no hot reload, for obvious reasons πŸ™ˆ )
  2. in devTools clear the queue and have "Track Widget Builds" activated
  3. push the "Select Page" Button
  4. in devTools -> refresh
video
Peek.2021-04-21.17-48.mp4

from riverpod.

davidmartos96 avatar davidmartos96 commented on May 10, 2024

@jlnrrg Do you need the shrinkWrap = true? If I'm not mistaken, that can be slow for long lists.
A second thing, are you testing it profile mode? Emulators can't run profile builds.

from riverpod.

jlnrrg avatar jlnrrg commented on May 10, 2024

Thank you for providing these hints.
Regarding shrinkWrap = true, this is an artifact from the real code, where the list gets actually quite large. But I now changed it in the example repo.

I was indeed not in profile mode, as I used the emulator for convenience.
But now I tried it on my phone in profile mode. And while the issue is less prone, it is still noticable.

proof picture

image

from riverpod.

ebelevics avatar ebelevics commented on May 10, 2024

I just recently migrated project from Bloc(Cubits) to Riverpod(StateNotifier), and while everything works fine, I can't call Future API methods right just before pushing new page inside AppRoutes:

    if (settings.name == HomeScreen.routeName) {
      context.read(baseDataProvider.notifier).getBaseData();
      return HomeScreen();
    }

before it was

    if (settings.name == HomeScreen.routeName) 
      return MultiBlocProvider(
        providers: [
          BlocProvider(
            create: (_) => BaseDataCubit(repository: sl<BaseRepository>())
              ..getBaseData(),
            lazy: false,
          ),
        ],
        child: HomeScreen(),
      );

otherwise I get error

E/flutter (27384): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Tried to use BaseDataNotifier after `dispose` was called.
E/flutter (27384):
E/flutter (27384): Consider checking `mounted`.

In this case I call getBaseData() to load base data from API to use in further screens (ex. HomeScreen -> SettingsScreen -> BaseDataScreen). But I'm not really sure how to do it.
The only way I see right now is using context.read(baseDataProvider.notifier).getBaseData(); in HomeScreen initState(), but I would like to avoid StatefullWidgets as much as possible.

from riverpod.

ebelevics avatar ebelevics commented on May 10, 2024

@rrousselGit thank you for quick reply
I have moved getBaseData inside BaseDataProvider, but yet it still dropped the error about Consider checking mounted.
Then I wrapped my HomeScreen() with Consumer widget and now seems it did the job.
Now baseDataProvider is initialized and tied with HomeScreen, but now I can access it in deeper stacked route screens, without worrying now about not reaching right context to access state, as it was with BlocProvider. Thank you very much :)

    if (settings.name == HomeScreen.routeName) {
      return Consumer(
        builder: (_, watch, __) {
          watch(baseDataProvider);
          return HomeScreen();
        },
      );
    }
final baseDataProvider =
    StateNotifierProvider.autoDispose<BaseDataNotifier, BaseDataState>(
  (ref) {
    final baseDataNotifier = BaseDataNotifier(repository: sl<BaseRepository>());
    baseDataNotifier.getBaseData();
    return baseDataNotifier;
  },
);

class BaseDataNotifier extends StateNotifier<BaseDataState> {
...

I don't know is it the correct approach, but it does the initialization without errors.

P.S. I was also worried that sl() [get_it] would affect it, but it wasn't the case.

from riverpod.

ardeaf avatar ardeaf commented on May 10, 2024

However, now my TodoListManager will need to add conditions to all its operations to check if the data has been loaded.

If you really don't care about it, then just put it at the top level of your scaffold a la:

    return Scaffold(
      body: asyncProvider.when(error: (Object error, StackTrace? stackTrace) {
        logger.warning(
            "Error when trying to load asyncProvider: $error. \nStacktrace: $stackTrace");
      }, loading: () {
        const Text("Loading");
      }, data: (data) {
        return Text("Loaded stuff is $data")
      });

If you aren't handling the situations where data is still loading then I feel like that's a fundamental mistake. Also, testing wouldn't be any different unless I'm missing something.

It would be a lot easier if I could wrap the whole thing in something that deals with the async loads, and from that point on just have a StateNotifier that works with the loaded data

Correct me if I'm wrong, but you can definitely do that, just have one of your higher level StatefulWidgets load it in initState() by using yourAsyncFunction().then((data) => stateNotifierProvider.setStateFunction(data)); or something similar to that and then you're good to go. You would lose all of the functionality of being able to add loading or error widgets though

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024

If you aren't handling the situations where data is still loading then I feel like that's a fundamental mistake.

I want something to worry about the loading and error cases, just not that StateNotifier.

Correct me if I'm wrong, but you can definitely do that, just have one of your higher level StatefulWidgets load it in initState() by using yourAsyncFunction().then((data) => stateNotifierProvider.setStateFunction(data)); or something similar to that and then you're good to go.

I don't think I quite understand what you mean here, are you suggesting setting the state of an already existing provider as an async callback in initState, or creating a new provider in initState? It seems like the former, in which case, sure, but the provider will still have to deal with potentially absent data (if state is being set it has already been created, after all), null at the very least. It also doesn't really address the broader issue that we can't chain a StateNotifier to something async without making the state async.

I hadn't thought about the latter, but it gave me an idea. Can I have a provider create another provider? I don't really understand this well enough under the hood to know if that would work just fine or subtly leak memory. This, for instance, seems to run as expected:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Consumer(
            builder: (context, ref, child) {
              final asyncManagerProvider = ref.watch(loaderProvider);
              return asyncManagerProvider.when(data: (data) {
                final todos = ref.watch(data);
                final manager = ref.watch(data.notifier);
                return ElevatedButton(
                  onPressed: () => manager.add(),
                  child: Text('There are ${todos.length} todos.'),
                );
              }, error: (error, stack) {
                return const Text('ERROR');
              }, loading: () {
                // Display loading
                return const CircularProgressIndicator();
              });
            },
          ),
        ),
      ),
    );
  }
}

class Todo {}

class TodoManager extends StateNotifier<List<Todo>> {
  TodoManager(List<Todo> todos) : super(todos);

  void add() {
    state = [
      Todo(),
      ...state,
    ];
  }
}

Future<List<Todo>> loadTodos() async {
  await Future.delayed(const Duration(milliseconds: 2000));
  return <Todo>[];
}

final loaderProvider = FutureProvider((ref) async {
  final todos = await loadTodos();
  final manager = TodoManager(todos);
  return StateNotifierProvider<TodoManager, List<Todo>>((ref) => manager);
});

This is essentially what I'm looking for, the only minor qualm being that I'd like ref.watch(loaderProvider); to return an AsyncValue over List<Todo>, and then having a ref.watch(loaderProvider.notifier) like StateNotifierProvider, which returns an AsyncValue<TodoManager>. I suppose I could write that class, actually (you have no idea how many times I've already rewritten this comment after having another thought πŸ˜…). I don't know if this could blow up in my face somehow, though.

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024

The whole point of the above, I should add, is that you can create a StateNotifier from an async value and listen to it with ref.watch(...), without the StateNotifier itself needing to know its input was async. Anything in the widget tree below the point where the notifier was instantiated can just ref.watch the created provider directly if they have access to the resolved AsyncValue, or retrieve the AsyncValue and safely assume it has resolved to data.

from riverpod.

ElteHupkes avatar ElteHupkes commented on May 10, 2024

Okay one more before I stop polluting this issue, I completely forgot about ProviderScope, but I feel like this is probably the best solution:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Consumer(
            builder: (context, outerRef, child) {
              final asyncManagerProvider =
                  outerRef.watch(futureTodoManagerProvider);
              return asyncManagerProvider.when(data: (data) {
                return ProviderScope(
                  overrides: [todoManagerProvider.overrideWithValue(data)],
                  // Let's pretend we're nested deep in the widget tree
                  child: Consumer(
                    builder: (context, ref, child) {
                      final todos = ref.watch(todoManagerProvider);
                      final manager = ref.watch(todoManagerProvider.notifier);

                      return ElevatedButton(
                        onPressed: () => manager.add(),
                        child: Text('There are ${todos.length} todos.'),
                      );
                    },
                  ),
                );
              }, error: (error, stack) {
                return const Text('ERROR');
              }, loading: () {
                // Display loading
                return const CircularProgressIndicator();
              });
            },
          ),
        ),
      ),
    );
  }
}

class Todo {}

class TodoManager extends StateNotifier<List<Todo>> {
  TodoManager(List<Todo> todos) : super(todos);

  void add() {
    state = [
      Todo(),
      ...state,
    ];
  }
}

Future<List<Todo>> loadTodos() async {
  await Future.delayed(const Duration(milliseconds: 2000));
  return <Todo>[];
}

final futureTodoManagerProvider = FutureProvider((ref) async {
  final todos = await loadTodos();
  return TodoManager(todos);
});

final todoManagerProvider = StateNotifierProvider<
    TodoManager,
    List<
        Todo>>((ref) => throw UnimplementedError(
    "Access to a [TodoManager] should be provided through a [ProviderScope]."));

Load the manager in the outer scope that deals with the loading state, provide it to the inner scope through an "abstract" provider. No extra provider magic needed, listening is simpler too. This feels like a proper solution rather than a hack.

from riverpod.

ardeaf avatar ardeaf commented on May 10, 2024

I don't think I quite understand what you mean here, are you suggesting setting the state of an already existing provider as an async callback in initState, or creating a new provider in initState? It seems like the former, in which case, sure, but the provider will still have to deal with potentially absent data (if state is being set it has already been created, after all), null at the very least.

I was only advocating this method because you said you wanted to 1) get data from some async function and 2) store that data in a statenotifier without requiring the use of AsyncValue. What I posted would accomplish that, I think. I suggested using initState() of a stateful widget because that method fires only once upon widget creation. It doesn't necessarily have anything to do with that widget's state. You're just putting a side effect in some widget's initState()

Ultimately, I think the correct way to go about this is doing what rousselGit suggested here because then you gain the added functionality of having your app be interactable while the async function is running in the background. I also think you're creating more work for yourself by having two providers, but if it works, it works. Don't you run into your same original issue of having to manager the loading, error, data states in your app though?

from riverpod.

ebelevics avatar ebelevics commented on May 10, 2024

This is what I created to bypass this limitation, and with futures delayed to zero it works great.

Problem lies when I tried to call function without using Future (skipFuture enabled) which is needed in one page as in function I asign AsyncValue.loading to state at start. But I tried to get error and I can't anymore. It was something like (widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children,).

but you can remove skipFuture from widget and call function from Future.delayed(Duration.zero, and it just works.

class MyScopeProvider extends ConsumerStatefulWidget {
  final Function(WidgetRef) call;
  final bool skipFuture;
  final Function(WidgetRef)? onDispose;
  final Widget child;
  const MyScopeProvider({required this.call, this.skipFuture = false, this.onDispose, required this.child, Key? key})
      : super(key: key);

  @override
  _MyScopeProviderState createState() => _MyScopeProviderState();
}

class _MyScopeProviderState extends ConsumerState<MyScopeProvider> {
  @override
  void initState() {
    if (widget.skipFuture) {
      widget.call(ref);
    } else {
      Future.delayed(Duration.zero, () async => widget.call(ref));
    }

    super.initState();
  }

  @override
  void dispose() {
    if (widget.onDispose != null) widget.onDispose!(ref);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

and call it with

MyScopeProvider(
            call: (ref) => ref.read(someProvider.notifier).doSomeAsyncFunction(),
            child: const SomeScreen(),
          ),

from riverpod.

a1573595 avatar a1573595 commented on May 10, 2024
Future<int> fetch() aynsc => 42;

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());
  }
}

Don't use 'const' on AsyncValue.loading(), It will make object the same and cannot be updated,
another way is override the updateShouldNotify.

from riverpod.

aymendn avatar aymendn commented on May 10, 2024
Future<int> fetch() aynsc => 42;

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());
  }
}

2 years later, is this still the recommended way to do it?

from riverpod.

mcrio avatar mcrio commented on May 10, 2024

@rrousselGit You expect some updates in the near future? The nested ProviderScope solution mentioned by @ElteHupkes looks very clean. I remember there were mentions about possible undesirable effects of nesting ProviderScope but can't find the discussion at this moment.

from riverpod.

mcrio avatar mcrio commented on May 10, 2024

@rrousselGit Thanks, AsyncNotifier does the job.

from riverpod.

mgwrd avatar mgwrd commented on May 10, 2024

@rrousselGit Thanks, AsyncNotifier does the job.

Could you provide an example @mcrio ?

I'm using AutoDisposeAsyncNotifier and awaiting a provider with await ref.watch(exampleProvider.future), but it throws the error mentioned in this issue: #1920

Example code that throws the error:

class ExampleNotifier extends AutoDisposeAsyncNotifier<ExampleData> {
  @override
  FutureOr<ExampleData> build() async {
    final data = await ref.watch(exampleDataProvider.future);
    final data2 = await ref.watch(exampleData2Provider.future);
    return ExampleData(dataValue: data, data2Value: data2);
  }
}

from riverpod.

mcrio avatar mcrio commented on May 10, 2024

@mgwrd Your code looks ok. Sorry no idea what might be wrong.

from riverpod.

a1573595 avatar a1573595 commented on May 10, 2024

ε₯½ε§AsyncNotifierοΌŒι›–η„Άζ²’ζœ‰θ¨˜ιŒ„γ€‚ 所δ»₯ι€™ε€‹ε•ι‘Œζ‡‰θ©²θ§£ζ±Ί

AsyncNotifier is good, thanks.

from riverpod.

heafox avatar heafox commented on May 10, 2024
class PlaylistNotifier extends StateNotifier<AsyncValue<List<Song>>>

  Future<void> prependSong(Song song) async {
    ...
    state = AsyncValue.data([song, ...state.value!]);
  }

  Future<void> removeSong(int index) async {
    ...
    state = AsyncValue.data(List.from(state.value!)..remove(song));
  }

Is there a better way?

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024

You are currently meant to use AsyncNotifier instead of StateNotifier.

StateNotifier is a bit out of date

from riverpod.

rrousselGit avatar rrousselGit commented on May 10, 2024

Closing since AsyncNotifier should solve this.

There are separate issues for tracking better documentation of AsyncNotifier & redirecting the docs of StateNotifier to AsyncNotifier

from riverpod.

SaddamMohsen avatar SaddamMohsen commented on May 10, 2024

I was only advocating this method because you said you wanted to 1) get data from some async function and 2) store that data in a statenotifier without requiring the use of AsyncValue. What I posted would accomplish that, I think. I suggested using initState() of a stateful widget because that method fires only once upon widget creation. It doesn't necessarily have anything to do with that widget's state. You're just putting a side effect in some widget's initState()

Fair enough, it looks like I'm not doing a good enough job to clearly put in to words what I think is causing the confusion. So let me try to paint a scenario.

Let's say I'm working on a TODO list app. I have a page where TODOs are displayed, added and deleted, and a page that displays TODO statistics. Everything is set up using mock data for now. Here's the provider for the statistics page:

final statsProvider = Provider((ref) {
  final todos = ref.watch(mockRepository).getTodos(); 
  return Statistics(todos);
}

ref.watch(statsProvider); // Statistics

For the list/add/edit/delete page I have a TodoManager extends StateNotifier<List<Todo>>. Here's its provider:

final todoManagerProvider = StateNotifierProvider<TodoManager, List<Todo>>((ref) {
 final todos = ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

ref.watch(todoManagerProvider); // List<Todo>
ref.watch(todoManagerProvider.notifier); // TodoManager

The whole UI is set up and working fine, now I'm ready to hook up real data. I come to the conclusion that in order to load the TODOs, I need an async call. So, I swap out the statistics provider for a FutureProvider:

final statsProvider = FutureProvider((ref) {
  final todos = await ref.watch(mockRepository).getTodos(); 
  return Statistics(todos);
}

ref.watch(statsProvider); // AsyncValue<Statistics>

Of course the UI will have to deal with the output being AsyncValue<Statistics>, but I'm managing the dependencies in the same place, and everything else stays roughly the same. Now it's time for the StateNotifier, and here's where I think people get confused. Based on the above, I'd say what people most likely expect is the following:

// THIS DOESN'T WORK
// ...for whoever is just skimming the code blocks
final todoManagerProvider = FutureStateNotifierProvider<TodoManager, List<Todo>>((ref) async {
 final todos = await ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

ref.watch(todoManagerProvider); // AsyncValue<List<Todo>>
ref.watch(todoManagerProvider.notifier); // AsyncValue<TodoManager>

Again the UI will have to deal with those AsyncValues again, but otherwise everything stays the same.

Instead, the solution requires moving all async initialization into the TodoManager, which can quite drastically change how that class behaves internally, because every add / delete / read operation needs to be aware of the loading status of the data. It requires a restructuring of the code on both ends. Now I haven't quite made up my mind on whether or not this is justified (there's maybe something fundamentally different about that last pretend code), but it certainly is unexpected. It may just be a matter of documentation. I'd be happy to help there by the way, I'm already producing pages of prose here anyway πŸ˜›.

All that being said

What I wanted for my actual code was for everything internally to stay the same, and for nested widgets to have easy access to an already loaded StateNotifier through ref.watch(). In my case that StateNotifier is also part of a .family, and I didn't really want to pass either that StateNotifier or the .family parameter that creates it down the tree. It was that last part that made me realize I could use a ProviderScope to solve the issue. Applied to the previous, my solution now is:

final futureTodoManagerProvider = FutureProvider((ref) async {
 final todos = await ref.watch(mockRepository).getTodos();
 return TodoManager(todos);
});

final todoManagerProvider = StateNotifierProvider<TodoManager, List<Todo>>((ref) => 
   throw UnimplementedError("Not provided"));

Widget someBuildMethod(BuildContext context, ref) {
  final manager = ref.watch(futureTodoManagerProvider);
  return manager.when(
    error: (_, __) => Text('ERROR'),
    loading: () => Text('LOADING'),
    data: (data) => ProviderScope(
      overrides: [todoManagerProvider.overrideWithValue(data)],
      child: Container(/* Everything in here is the same as before */),
    ),
  );
}

This does everything I need it to, including giving me the ability to simply ref.watch(todoManagerProvider) anywhere in the nested widget tree, even when that original TodoManager was part of a family.

i think your solution is the best one fitted to my problem ,but when i am decided to use it new problem emerged the overrideWithValue(data) function is deleted so how to repair this issue

from riverpod.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.