-
-
Notifications
You must be signed in to change notification settings - Fork 923
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
Get an asynchronously initialized object to a Provider #329
Comments
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 |
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. |
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 #329 (comment)? 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.
|
@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:
|
@wujek-srujek
Others have also requested being able to combine |
Just one more question if I may, and I'm off to my next endeavours: with this provider opening a final boxFutureProvider = FutureProvider<Box>((_) async {
await Hive.initFlutter();
return Hive.openBox('storage');
}); how can I make sure tha final boxFutureProvider = FutureProvider.autoDispose<Box>((_) async {
await Hive.initFlutter();
return Hive.openBox('storage');
}); I get an error in boxProvider; ref.watch(boxFutureProvider).maybeWhen...
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 The |
I played around with the code and I get it now: I don't fully understand why |
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 |
+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 @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 |
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 |
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 |
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 You can use |
Would be nice if there were a built-in completer to use in the // 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;
}); |
This is built directly into 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(),
),
);
} |
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. 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)
} |
Why would |
Oh oops I somehow missed that. Thanks! |
You know that you can call runApp more than once right? final prefsProvider = FutureProvider<SharedPreferences>((ref) {
return SharedPreferences.getInstance();
});
void main() async {
// Show a progress indicator while awaiting things
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
),
);
final container = ProviderContainer();
await container.read(prefsProvider.future);
runApp(
UncontrolledProviderScope(
container: container,
child: MyApp(),
),
);
} Personally I would also make those asynchronous providers synchronous like this: final prefsProvider = Provider<SharedPreferences>((ref) {
throw Exception('Provider was not initialized');
});
void main() async {
// Show a progress indicator while awaiting things
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
),
);
final prefs = await SharedPreferences. getInstance();
runApp(
ProviderScope(
overrides: [
prefsProvider.overrideWithValue(prefs),
],
child: MyApp(),
),
);
} |
@TimWhiting I didn't know about calling runApp multiple times. Thanks for sharing! |
I have a situation with two such FutureProviders that require non-trivial init steps. Both providers are top-level providers that will only be created once in the app's lifetime. The second has a dependency on the first, amongst a few more. final localPreferences =
FutureProvider<LocalPreferencesService>((ref) async {
final prefs = LocalPreferencesService();
await prefs.init();
return prefs;
});
final dynamicLinkProvider =
FutureProvider<DynamicLinkService>((ref) async {
final auth = ref.watch(firebaseAuthProvider);
final prefs = ref.watch(localPreferencesProvider);
final errorMessages = ref.watch(errorMessagesServiceProvider);
final dynamicLinks = DynamicLinkService(
auth: auth, localPrefs: prefs, errorMessages: errorMessages);
await registerDynamicLinksHandler(dynamicLinks);
return dynamicLinks;
}); Copying the init code to
Parts 1 and 2 worked fine: final localPreferencesProvider = Provider<LocalPreferencesService>((ref) {
throw UnimplementedError();
});
final localPreferencesProviderReady =
FutureProvider<LocalPreferencesService>((ref) async {
final prefs = LocalPreferencesService();
await prefs.init();
return prefs;
});
final dynamicLinkProvider = Provider<DynamicLinkService>((ref) {
throw UnimplementedError();
});
final dynamicLinkProviderReady =
FutureProvider<DynamicLinkService>((ref) async {
final auth = ref.watch(firebaseAuthProvider);
final prefs = ref.watch(localPreferencesProvider);
final errorMessages = ref.watch(errorMessagesServiceProvider);
final dynamicLinks = DynamicLinkService(
auth: auth, localPrefs: prefs, errorMessages: errorMessages);
await registerDynamicLinksHandler(dynamicLinks);
return dynamicLinks;
}); Part 3 is harder. I require I tried coding it, but am getting errors: final container = ProviderContainer(
observers: [
const StateLogger(),
],
overrides: [
localPreferencesProvider,
dynamicLinkProvider,
],
);
final prefsInitialized =
await container.read(localPreferencesProviderReady.future);
container.updateOverrides([
localPreferencesProvider.overrideWithValue(prefsInitialized),
dynamicLinkProvider,
]);
final dynamicLinksInitialized =
await container.read(dynamicLinkProviderReady.future);
container.updateOverrides([
localPreferencesProvider.overrideWithValue(prefsInitialized),
dynamicLinkProvider.overrideWithValue(dynamicLinksInitialized),
]);
runZonedGuarded(
() => runApp(UncontrolledProviderScope(
container: container,
child: const Application(),
)),
crashlytics.recordError,
); And even if those errors were solvable - I am still left with a situation in which my initialization logic is divider between the providers that know the init steps and the Any thoughts? |
This works, but still duplicates the provider dependency logic in main: final container = ProviderContainer();
final prefs = await container.read(localPreferencesProviderReady.future);
final container2 = ProviderContainer(
overrides: [
localPreferencesProvider.overrideWithValue(prefs),
],
);
final dynamicLinks = await container2.read(dynamicLinkProviderReady.future);
final container3 = ProviderContainer(
observers: [
const StateLogger(),
],
overrides: [
localPreferencesProvider.overrideWithValue(prefs),
dynamicLinkProvider.overrideWithValue(dynamicLinks),
],
);
runZonedGuarded(
() => runApp(UncontrolledProviderScope(
container: container3,
child: const Application(),
)),
crashlytics.recordError,
); |
The above solution, however, creates some of the providers (e.g. |
Initially, I don't have values with which to override the two providers, so creating a single container and calling final container = ProviderContainer(overrides: [
localPreferencesProvider.overrideWithValue(null as LocalPreferencesService),
dynamicLinkProvider.overrideWithValue(null as DynamicLinkService),
]);
final prefs = await container.read(localPreferencesProviderReady.future);
container.updateOverrides([
localPreferencesProvider.overrideWithValue(prefs),
dynamicLinkProvider.overrideWithValue(null as DynamicLinkService),
]);
final dynamicLinks = await container.read(dynamicLinkProviderReady.future);
container.updateOverrides([
localPreferencesProvider.overrideWithValue(prefs),
dynamicLinkProvider.overrideWithValue(dynamicLinks),
]);
runZonedGuarded(
() => runApp(UncontrolledProviderScope(
container: container,
child: const Application(),
)),
crashlytics.recordError,
); @rrousselGit this is a use case in which |
For now I pass mock objects that I create using mocktail. |
What do you mean by "I don't have values with which to override the providers"? Why do you override the providers in this case? |
I want:
Since a container only allows updating of overrides, I must initially override |
Rather than updating existing overrides, you probably want to create a child ProviderContainer: final root = ProviderContainer();
addTearDown(root.dispose);
final prefs = await root.read(myprovider.future);
final container = ProviderContainer(parent: root, overrides: [
syncPrefsProvider.overrideWithValue(prefs),
]); |
AsyncNotifier was added for this purpose. It is a StateNotifier with asynchronous initialization Docs about it are here https://docs-v2.riverpod.dev/docs/essentials/side_effects |
So, how does that help to provide synchronous value since asynchronous initialization returns |
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 aListener
(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):I have tried 2 approaches and I hit a wall with both:
a) I initialize
Hive
inmain
:but don't know how to pass the
box
to my provider:(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
:but now this line doesn't work:
I need to do it like this:
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:but now I have a
Future<Box>
and I can't useawait
, so this won't work.I tried making
globalStateProvider
aFutureProvider
as well so that I canawait
:but now I need to deal with
Future<GlobalState>
whenever I want to useglobalStateProvider
:which makes it pretty complicated. I also don't know if simply using a
FutureProvider
instead ofStateNotifierProvider
degrades the features that I get (I simply know Riverpod too little).How can/should I solve my use case?
The text was updated successfully, but these errors were encountered: