riverpod
riverpod copied to clipboard
Get an asynchronously initialized object to a Provider
I would like to make the state managed by a StateNotifier
be persistent and survive app restarts.
Is there anything out of the box that I could use, so that when the state
is changed/set, it automatically gets saved? Or do I need to implement it myself using a Listener
(or some other mechanism, please advise)?
So far I haven't found anything built-in, so I think I need to do it myself. I decided to try doing it with Hive
, but I don't know how to actually do it. This is the base code (no persistence):
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
@immutable
class GlobalState {
GlobalState();
factory GlobalState.fromJson(Map<String, dynamic> json) {
return null; // irrelevant
}
Map<String, dynamic> toJson() => null; // irrelevant
}
class GlobalStateNotifier extends StateNotifier<GlobalState> {
GlobalStateNotifier(GlobalState state) : super(state);
@override
GlobalState get state => super.state;
}
final globalStateProvider = StateNotifierProvider(
(ref) => GlobalStateNotifier(GlobalState()),
);
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHome(),
),
);
}
}
class MyHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, watch, _) {
final state = watch(globalStateProvider).state;
return Scaffold(
appBar: AppBar(
title: Text('Test'),
),
body: Center(
child: Text('$state'),
),
);
},
);
}
}
I have tried 2 approaches and I hit a wall with both:
a) I initialize Hive
in main
:
void main() async {
await Hive.initFlutter();
final box = Hive.box('storage');
runApp(MyApp());
}
but don't know how to pass the box
to my provider:
final globalStateProvider = StateNotifierProvider<GlobalStateNotifier>(
(ref) {
final Box box = null; // How to get the box initialized in main()?
final json = box.get('appState');
return GlobalStateNotifier(GlobalState.fromJson(json));
},
);
(I understand that in this file I can just assign to a global variable in main and use it in the provider, but in reality these would be separate files and I don't want any mutable global state.)
I tried to do it with family
:
final globalStateProvider =
StateNotifierProvider.family<GlobalStateNotifier, Box>(
(ref, box) {
final json = box.get('appState');
return GlobalStateNotifier(GlobalState.fromJson(json));
},
);
but now this line doesn't work:
final state = watch(globalStateProvider).state;
I need to do it like this:
final state = watch(globalStateProvider(box)).state;
in every place I want to watch it, i.e. I need access to the box, which I don't think is the way it is supposed to work. I think I simply don't understand how families work.
b) I tried with FutureProvider
, like this:
final boxProvider = FutureProvider<Box>(
(ref) async {
await Hive.initFlutter();
return Hive.box('storage');
},
);
final globalStateProvider = StateNotifierProvider<GlobalStateNotifier>(
(ref) {
final futureBox = ref.watch(boxProvider.future);
// needs a Box but I have a Future<Box> and can't await
final json = box.get('appState');
return GlobalStateNotifier(GlobalState.fromJson(json));
},
);
but now I have a Future<Box>
and I can't use await
, so this won't work.
I tried making globalStateProvider
a FutureProvider
as well so that I can await
:
final globalStateProvider = FutureProvider<GlobalStateNotifier>(
(ref) async {
final box = await ref.watch(boxProvider.future);
final json = box.get('appState');
return GlobalStateNotifier(GlobalState.fromJson(json));
},
);
but now I need to deal with Future<GlobalState>
whenever I want to use globalStateProvider
:
final futureState = watch(globalStateProvider.future);
which makes it pretty complicated. I also don't know if simply using a FutureProvider
instead of StateNotifierProvider
degrades the features that I get (I simply know Riverpod too little).
How can/should I solve my use case?
How about:
final boxProvider = Provider<Box>((_) => null);
void main() {
await Hive.initFlutter();
final box = Hive.box('storage');
runApp(ProviderScope(overrides: [boxProvider.overrideWithProvider(Provider((_) => box))], child: MyApp()));
}
This is off the top of my head. I might have gotten the syntax wrong.
Elsewhere you can use/read/watch boxProvider
.
It actually did occurr to me but feels like a hack I would not shy away from in tests, but don't want in my productivity code if I can help it.
Do I get it right, is what I'm trying to do not yet supported 'natively' by riverpod? I wouldn't think it is such an exotic use case, and the two ways I explored have an extremely high penalty of forcing me to change my code at every usage site.
Maybe it would help if it were possible to pass 'external' objects into ProviderScope and somehow look them up using ref? Pretty much what your solution looks like, but without the dummy provider and a dedicated syntax/parameter?
@wujek-srujek I've also had this same question when trying out riverpod, but to handle opening an Sqlite Database. The opening is async, but all providers should ideally access the opened database (no Future) because the opening is awaited with a Progress Indicator before starting the rest of the app.
I came up with somewhat of a solution.
Create a future provider that would handle the async intialization. Then have a separate provider that watches the future provider returning null while the future is loading.
^ That would also work. There are several ways of doing this. Personally I feel like my solution is the simplest in terms of lines of code. But you can also do what @davidmartos96 suggested & here is the code maybe:
final boxFutureProvider = FutureProvider<Box>((_) async {
await Hive.initFlutter();
return Hive.box('storage');
});
final boxProvider((ref) => ref.watch(boxFutureProvider).maybeWhen(
data:(d) => d,
orElse: () => throw Exception('Box uninitialized'),
));
void main() {
runApp(ProviderScope(child: MyApp()));
}
I would also make sure that you don't access the provider if the future is not initialized by having some loading widget:
Consumer(
builder: (context, watch, _) {
final boxState = watch(boxFutureProvider);
return boxState.maybeWhen(data:(d) => YourWidgetTree(), orElse: () => CircularProgressIndicator());
}
)
Again this is just off the top of my head, so no guarantees on syntax.
@wujek-srujek if the async operation is known to be short (obtaining the Sharedpreferences instance for example) then I would go with the simple snippet from Tim, awaiting it in main. If the operation may take longer, like opening a database, then I would recommend going with the latest sample, so that the user can be aware that something is loading.
Agreed, that is my feeling as well.
@davidmartos96 Which 'latest' sample do you have in mind? Could you share the solution you came up with and mentioned in here https://github.com/rrousselGit/river_pod/issues/329#issuecomment-778698018?
I still can't wrap my head around it. If I have this:
final boxProvider((ref) => ref.watch(boxFutureProvider).maybeWhen(
data:(d) => d,
orElse: () => throw Exception('Box uninitialized'),
));
How can I then use it in a different provider that needs the box? Will the provider get an exception? What would the code in that provider look like?
@wujek-srujek Tim created a snippet for both approaches in this thread.
-
Simple and very short async calls. Await the result before running the app and provide the value via the ProviderScope.
-
Longer async calls. Create both a Future provider and a regular provider and watch the future provider in the UI to display a progress indicator while it's loading
@rrousselGit You asked people for use cases that are harder to implement. Would this be one of them, to warrant documentation/example/FAQ, or maybe even dedicated support at the library level? This seems common enough a scenario.
@wujek-srujek You would get an exception if you watch/read the value of that provider before the futureprovider has finished. So if you wait for the future provider before showing the rest of the UI you should be fine.
Ok, thanks to your help I made it work, both ways. Some observations/questions:
- The solution with
boxProvider.overrideWithProvider(Provider((_) => box)
definitely feels like a hack, but is short. I noticed that I can also writeboxProvider.overrideWithValue(box)
to make it even shorter - am I missing out on something if I do this? - The solution with
boxFutureProvider
+boxProvider
+Consumer
+maybeWhen
feel right, but is a bit more complex. Does it scale? Suppose I have 5 resources like this that I want to load at startup - would I do it in a single 'gateway' provider or 5 separate ones? Is there a way to combineAsyncValue
, like e.g. https://api.flutter.dev/flutter/dart-async/Future/wait.html?
@wujek-srujek Yes your 1. also works. I don't think you are missing out unless there is other logic that needs to happen when the provider is created for the first time.
- Yes it does feel right, because you are handling all of the cases that should be user visible (possibly even an error state, which would be good to handle separately from loading). For multiple resources I would maybe create a widget that just takes any generic FutureProvider, waits for it to become available and shows a child if the future is complete. This way you can show separate error / loading messages depending on what resource you are waiting for (i.e. some might be long --> syncing with a remote database --> longer progress bar or something, while other maybe short --> loading spinner).
Others have also requested being able to combine AsyncValue
see issue #67 for that discussion.
Just one more question if I may, and I'm off to my next endeavours: with this provider opening a Box
:
final boxFutureProvider = FutureProvider<Box>((_) async {
await Hive.initFlutter();
return Hive.openBox('storage');
});
how can I make sure tha Box.close
is called? When I change it to:
final boxFutureProvider = FutureProvider.autoDispose<Box>((_) async {
await Hive.initFlutter();
return Hive.openBox('storage');
});
I get an error in boxProvider;
ref.watch(boxFutureProvider).maybeWhen...
The argument type 'AutoDisposeFutureProvider<Box<dynamic>>' can't be assigned to the parameter type 'AlwaysAliveProviderBase<Object, dynamic>'.
Is something like this correct code?
final boxFutureProvider = FutureProvider<Box>((ref) async {
await Hive.initFlutter();
final box = await Hive.openBox('storage');
ref.onDispose(() {
box.close();
});
return box;
});
If a provider you depend on is .autoDispose
then yourself and all of your dependents must also be .autoDispose
. In this case making boxFutureProvider
an AutoDisposeFutureProvider
means that you have to make boxProvide
r a disposable provider as well. Does that make sense?
The ref.onDispose
is available even for non autoDispose
providers because even if the providers don't dispose because of no longer being depended on by child providers / widget tree, they might still be rebuilt based on other conditions (i.e. a provider they are watching is rebuilt causing them to rebuild).
I played around with the code and I get it now: autoDispose
and ref.onDispose
are different things, the former being a way to automatically dispose providers when nobody is listening, and the latter allowing me to execute custom actions when the provider is being disposed. ref.onDispose
can be triggered by autoDispose
-ing, but doesn't have to be.
I don't fully understand why autoDispose
is 'contagious' - if provider A is autoDispose
, and provider B watches it, why does B have to be autoDispose
as well? As long as B watches A, A can't and won't be auto-disposed. When B is disposed (no matter how it happens), if it was the last watcher of A, A will be disposed; otherwise, A will not be disposed. I don't understand why this behavior of A changes B and all of its dependents (direct or transitive).
It is contagious because otherwise there is no point to have a provider be autoDispose if it is always going to be kept alive by a non auto dispose provider.
Yeah but is being autoDispose
the only way for a provider to be disposed? If that's the case, then yes, it makes sense. But I though there might be other ways, like explicitly calling some kind of dispose
method or something?
+1 to this issue. I am also facing a similar issue and I think this will be a common use case across most of the app where we need to have one-time future providers Initialization before the app starts.
I want a shareprefernce and database one time.
final sharePrefProvider = FutureProvider((ref) => SharedPreferences.getInstance());
final hiveDatabaseInfo = FutureProvider((ref) => Hive.openBox('databaseInfo'));
final appDatabaseProvider = Provider<AppDatabase>((ref) {
final prefsProvider = ref.watch(hiveDatabaseInfo);
return AppDatabaseImpl(prefsProvider.data?.value);
});
final appSettingProvider = Provider.autoDispose((ProviderReference ref) {
final prefsProvider = ref.watch(sharePrefProvider);
return AppSettings(prefsProvider.data?.value);
});
Same as this comment I don't want to propagate that Future
and AsyncValue
to other providers and also to the UI code.
@TimWhiting Solutions to override provide works but it still looks like a hack and which needs to more explicitly document in the code.
Thinking out loud.....we can think of a provider that forces to convert FutureProvider
to Provider
without managing Future
and AsyncValue
in the second provider. The provider will wait until that FutureProvider is completed.
Or a mechanism to await inside a Provider .
Thinking out loud.....we can think of a provider that forces to convert FutureProvider to Provider without managing Future and AsyncValue in the second provider.
Rather than converting FutureProvider to Provider, you may want to do the opposite and have:
final sharePrefProvider = FutureProvider((ref) => SharedPreferences.getInstance());
final hiveDatabaseInfo = FutureProvider((ref) => Hive.openBox('databaseInfo'));
final appDatabaseProvider = FutureProvider<AppDatabase>((ref) async {
final prefs = await ref.watch(hiveDatabaseInfo.future);
return AppDatabaseImpl(prefs);
});
final appSettingProvider = FutureProvider.autoDispose((ref) async {
final prefs = await ref.watch(sharePrefProvider.future);
return AppSettings(prefs);
});
Thinking out loud.....we can think of a provider that forces to convert FutureProvider to Provider without managing Future and AsyncValue in the second provider.
Rather than converting FutureProvider to Provider, you may want to do the opposite and have:
final sharePrefProvider = FutureProvider((ref) => SharedPreferences.getInstance()); final hiveDatabaseInfo = FutureProvider((ref) => Hive.openBox('databaseInfo')); final appDatabaseProvider = FutureProvider<AppDatabase>((ref) async { final prefs = await ref.watch(hiveDatabaseInfo.future); return AppDatabaseImpl(prefs); }); final appSettingProvider = FutureProvider.autoDispose((ref) async { final prefs = await ref.watch(sharePrefProvider.future); return AppSettings(prefs); });
Yes, but everytime, you a re trying to get appDatabaseProvider or appSettingProvider..you will have to handle loading/error etc..?
Agree with @Abacaxi-Nelson comment. This solution will force to handle loading/error every-time we access the appDatabaseProvider
which is actually already loaded when app started.
If your futures are loaded before the app start, load them in the main rather than in a FutureProvider
If your futures are loaded before the app start, load them in the main rather than in a FutureProvider
The problem comes when the async initialization takes a bit of time, and one wants to show a progress indicator (just once)
Then the initialization isn't done before the app start, so you need to deal with loading/error within the application
I don't understand the problem. You don't have to use when(data: ..., loading: ..., error: ...)
everywhere
Then the initialization isn't done before the app start, so you need to deal with loading/error within the application
I don't understand the problem. You don't have to use
when(data: ..., loading: ..., error: ...)
everywhere
You are saying do async task once and pass down the result to the tree ? We will not be able to use the context.read :(
I'm saying that AsyncValue
exposes a way to obtain the value without when
You can use asyncValue.data!.value
if you don't want to deal with loading state in this place
Would be nice if there were a built-in completer to use in the main()
function then. Currently, my workaround is:
// in main()
container.read(prefsProvider); // trigger initialisation
await prefsInitCompleter.future; // wait for initialisation to complete
// in provider code
final prefsInitCompleter = Completer<void>();
final prefsProvider = FutureProvider<SharedPreferences>((ref) async {
final prefs = await SharedPreferences.getInstance();
prefsInitCompleter.complete();
return prefs;
});
Would be nice if there were a built-in completer to use in the main() function then. Currently, my workaround is:
This is built directly into FutureProvider
:
final prefsProvider = FutureProvider<SharedPreferences>((ref) {
return SharedPreferences.getInstance();
});
void main() async {
final container = ProviderContainer();
await container.read(prefsProvider.future);
runApp(
UncontrolledProviderScope(
container: container,
child: MyApp(),
),
);
}
asyncValue.data!.value
My first solution was this only. But then If any other provider depends on it than they also need to handle the null value or make the dependencies as nullable which is not I wanted to do. Ex
final appDatabaseProvider = Provider<AppDatabase>((ref) {
final prefsProvider = ref.watch(hiveDatabaseInfo);
return AppDatabaseImpl(prefsProvider.data!.value);
});
class AppDatabaseImpl{
final Box? box // I want to avoid nullable dependencies
AppDatabaseImpl(this.box)
}