-
-
Notifications
You must be signed in to change notification settings - Fork 956
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
Comments
Is extending StateNotifier<AsyncValue> what you're looking for? |
Can you provide a simple example? I am quite confused about how it should be done. Thank you 🙏🏻 |
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());
}
} |
Oh I see!!! That should do it, yes. That really helped. Thank you so much for everything! |
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. |
You could override the setter of |
Yes... But look like a difficult solution to something quite usual. Maybe would be better to create a FutureProvider apart to handle update logic. |
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). |
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. |
For simple Future<int> fetch() aynsc => 42;
final Whatever = FutureProvider<int> async{
final result = await fetch();
return result;
} and |
I see. but in your use-case, I think you don't need to use StateNotifier, use FutureProvider is enough. |
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 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 {}
} |
@rrousselGit StateNotifierfinal 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));
}
} WidgetScaffold(
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)
);
}),
); |
That isn't the case. Could you provide a full example of how to reproduce this performance issue? |
@rrousselGit sorry that is took a while. 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.
videoPeek.2021-04-21.17-48.mp4 |
@jlnrrg Do you need the shrinkWrap = true? If I'm not mistaken, that can be slow for long lists. |
Thank you for providing these hints. I was indeed not in profile mode, as I used the emulator for convenience. |
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:
before it was
otherwise I get error
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. |
@ebelevics Consider callling |
@rrousselGit thank you for quick reply
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. |
The suggestion
works, and isn't super complicated, but it would be quite nice if a For instance, let's say we have a class TodoListManager extends StateNotifier<List<Todo>> {...} And getting that initial final repositoryProvider = FutureProvider<Repository>((ref) async {
final database = await ref.watch(databaseProvider.future);
final repository = getRepository(database);
return repository;
}); Now, I can have my I guess a 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 |
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.
Correct me if I'm wrong, but you can definitely do that, just have one of your higher level StatefulWidgets load it in |
I want something to worry about the loading and error cases, just not that
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 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:
This is essentially what I'm looking for, the only minor qualm being that I'd like |
The whole point of the above, I should add, is that you can create a |
Okay one more before I stop polluting this issue, I completely forgot about 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. |
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 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 |
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 ( but you can remove skipFuture from widget and call function from 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(),
), |
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 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 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 // 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 Instead, the solution requires moving all async initialization into the All that being saidWhat 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 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 |
Don't use 'const' on AsyncValue.loading(), It will make object the same and cannot be updated, |
2 years later, is this still the recommended way to do it? |
Yes... But not for long 👀 |
@rrousselGit You expect some updates in the near future? The nested |
Well |
@mcrio The primary practical downside I experience is that a provider that uses another provider that is overridden in a ...I'm absolutely going to find out what |
@rrousselGit Thanks, |
Could you provide an example @mcrio ? I'm using Example code that throws the error:
|
@mgwrd Your code looks ok. Sorry no idea what might be wrong. |
AsyncNotifier is good, thanks. |
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? |
You are currently meant to use AsyncNotifier instead of StateNotifier. StateNotifier is a bit out of date |
Closing since AsyncNotifier should solve this. There are separate issues for tracking better documentation of AsyncNotifier & redirecting the docs of StateNotifier to AsyncNotifier |
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 |
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.
The text was updated successfully, but these errors were encountered: