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

Initialize StateNotifierProvider with async data #57

Closed
edmbn opened this issue Jul 23, 2020 · 43 comments
Closed

Initialize StateNotifierProvider with async data #57

edmbn opened this issue Jul 23, 2020 · 43 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@edmbn
Copy link

edmbn commented Jul 23, 2020

Describe what scenario you think is uncovered by the existing examples/articles

There is a simple use case. I want to create a todoList stateNotifierProvider that is initialized with todos fetched from API. If I fetch the todos on a initialize function inside the stateNotifier class I can't get the states of an asyncValue from a futureProvider (loading, error and data).

Describe why existing examples/articles do not cover this case

There aren't examples covering this type of cases where stateNotifierProviders are needed, to create a state and some logic to change this state, but getting the benefits of an asyncValue states returned by a futureProvider.

@edmbn edmbn added the documentation Improvements or additions to documentation label Jul 23, 2020
@rrousselGit
Copy link
Owner

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

@edmbn
Copy link
Author

edmbn commented Jul 23, 2020

Can you provide a simple example? I am quite confused about how it should be done. Thank you 🙏🏻

@rrousselGit
Copy link
Owner

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

@edmbn
Copy link
Author

edmbn commented Jul 23, 2020

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

@edmbn
Copy link
Author

edmbn commented Aug 7, 2020

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.

@rrousselGit
Copy link
Owner

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

@edmbn
Copy link
Author

edmbn commented Aug 8, 2020

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

@joanofdart
Copy link

joanofdart commented Aug 28, 2020

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).

@edmbn
Copy link
Author

edmbn commented Aug 28, 2020

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.

@tbm98
Copy link
Contributor

tbm98 commented Aug 29, 2020

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

@joanofdart
Copy link

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.

@tbm98
Copy link
Contributor

tbm98 commented Aug 29, 2020

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.

@cswkim
Copy link

cswkim commented Feb 11, 2021

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 {}
}

@jlnrrg
Copy link

jlnrrg commented Apr 21, 2021

@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

@rrousselGit
Copy link
Owner

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?

@jlnrrg
Copy link

jlnrrg commented Apr 21, 2021

@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

@davidmartos96
Copy link
Contributor

@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.

@jlnrrg
Copy link

jlnrrg commented Apr 22, 2021

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

@ebelevics
Copy link

ebelevics commented Sep 4, 2021

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.

@rrousselGit
Copy link
Owner

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

@ebelevics
Copy link

ebelevics commented Sep 5, 2021

@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.

@ElteHupkes
Copy link

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.

@ardeaf
Copy link

ardeaf commented Jan 13, 2022

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

@ElteHupkes
Copy link

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.

@ElteHupkes
Copy link

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.

@ElteHupkes
Copy link

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.

@ardeaf
Copy link

ardeaf commented Jan 13, 2022

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?

@ebelevics
Copy link

ebelevics commented Jan 13, 2022

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(),
          ),

@ElteHupkes
Copy link

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.

@a1573595
Copy link

a1573595 commented Jul 18, 2022

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.

@aymendn
Copy link

aymendn commented Aug 25, 2022

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?

@rrousselGit
Copy link
Owner

Yes... But not for long 👀

@mcrio
Copy link

mcrio commented Oct 10, 2022

@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.

@rrousselGit
Copy link
Owner

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

@ElteHupkes
Copy link

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.

@mcrio
Copy link

mcrio commented Oct 11, 2022

@rrousselGit Thanks, AsyncNotifier does the job.

@mgwrd
Copy link

mgwrd commented Dec 11, 2022

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

@mcrio
Copy link

mcrio commented Dec 13, 2022

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

@a1573595
Copy link

好吧AsyncNotifier,雖然沒有記錄。 所以這個問題應該解決

AsyncNotifier is good, thanks.

@heafox
Copy link

heafox commented Jan 3, 2023

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?

@rrousselGit
Copy link
Owner

You are currently meant to use AsyncNotifier instead of StateNotifier.

StateNotifier is a bit out of date

@rrousselGit
Copy link
Owner

Closing since AsyncNotifier should solve this.

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

@SaddamMohsen
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests