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

Get an asynchronously initialized object to a Provider #329

Closed
wujek-srujek opened this issue Feb 13, 2021 · 44 comments
Closed

Get an asynchronously initialized object to a Provider #329

wujek-srujek opened this issue Feb 13, 2021 · 44 comments
Assignees
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@wujek-srujek
Copy link

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?

@wujek-srujek wujek-srujek added bug Something isn't working needs triage labels Feb 13, 2021
@TimWhiting
Copy link
Collaborator

TimWhiting commented Feb 13, 2021

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.

@wujek-srujek
Copy link
Author

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.

@wujek-srujek
Copy link
Author

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?

@davidmartos96
Copy link
Contributor

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

@TimWhiting
Copy link
Collaborator

TimWhiting commented Feb 14, 2021

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

@davidmartos96
Copy link
Contributor

davidmartos96 commented Feb 14, 2021

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

@TimWhiting
Copy link
Collaborator

Agreed, that is my feeling as well.

@wujek-srujek
Copy link
Author

wujek-srujek commented Feb 14, 2021

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

@davidmartos96
Copy link
Contributor

davidmartos96 commented Feb 14, 2021

@wujek-srujek Tim created a snippet for both approaches in this thread.

  1. Simple and very short async calls. Await the result before running the app and provide the value via the ProviderScope.

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

@wujek-srujek
Copy link
Author

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

@davidmartos96
Copy link
Contributor

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

@wujek-srujek
Copy link
Author

Ok, thanks to your help I made it work, both ways. Some observations/questions:

  1. The solution with boxProvider.overrideWithProvider(Provider((_) => box) definitely feels like a hack, but is short. I noticed that I can also write boxProvider.overrideWithValue(box) to make it even shorter - am I missing out on something if I do this?
  2. 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 combine AsyncValue, like e.g. https://api.flutter.dev/flutter/dart-async/Future/wait.html?

@TimWhiting
Copy link
Collaborator

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

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

@wujek-srujek
Copy link
Author

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

@TimWhiting
Copy link
Collaborator

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

@wujek-srujek
Copy link
Author

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

@TimWhiting
Copy link
Collaborator

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.

@wujek-srujek
Copy link
Author

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?

@rrousselGit rrousselGit added enhancement New feature or request and removed needs triage bug Something isn't working labels Apr 17, 2021
@burhanrashid52
Copy link

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

@rrousselGit
Copy link
Owner

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

@Abacaxi-Nelson
Copy link

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

@burhanrashid52
Copy link

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.

@rrousselGit
Copy link
Owner

If your futures are loaded before the app start, load them in the main rather than in a FutureProvider

@davidmartos96
Copy link
Contributor

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)

@rrousselGit
Copy link
Owner

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

@Abacaxi-Nelson
Copy link

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 :(

@rrousselGit
Copy link
Owner

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

@skytect
Copy link

skytect commented Jul 13, 2021

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

@rrousselGit
Copy link
Owner

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

@burhanrashid52
Copy link

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

@rrousselGit
Copy link
Owner

Why would box be nullable in your example?
If you don't want it to be nullable make hiveDatabaseInfo return a non-nullable Box or do .value!

@skytect
Copy link

skytect commented Jul 13, 2021

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

Oh oops I somehow missed that. Thanks!

@TimWhiting
Copy link
Collaborator

The problem comes when the async initialization takes a bit of time, and one wants to show a progress indicator (just once)

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

@davidmartos96
Copy link
Contributor

@TimWhiting I didn't know about calling runApp multiple times. Thanks for sharing!

@rrousselGit rrousselGit added the documentation Improvements or additions to documentation label Oct 7, 2021
@amitkot
Copy link

amitkot commented Oct 28, 2021

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 main() feels like a hack, so I tried the following:

  1. Replace both FutureProviders with dummy implementations that throw
  2. DefineSomethingProviderReady future providers that do the actual job.
  3. Create a ProviderContainer and override the providers in it.

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 localPreferencesProvider to be initialized when setting up dynamicLinksProvider so I have to create and container, then override it with a localPreferencesProvider, then use it to create an initialized dynamicLinksProvider and finally override the container with both of them and run the app.

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 main() that has to maintain the order for overriding.

Any thoughts?

@amitkot
Copy link

amitkot commented Oct 28, 2021

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

@amitkot
Copy link

amitkot commented Oct 28, 2021

The above solution, however, creates some of the providers (e.g. firebaseAuthProvider that is required for dynamicLinksProvider) more than once.

@amitkot
Copy link

amitkot commented Oct 28, 2021

Initially, I don't have values with which to override the two providers, so creating a single container and calling updateOverrides() is a problem. e.g. this doesn't work:

  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 overrideWithProvider would have come handy.

@amitkot
Copy link

amitkot commented Oct 28, 2021

For now I pass mock objects that I create using mocktail.
It works okay, but quite a complicated solution (and the dependency logic is still duplicated in main).

@rrousselGit
Copy link
Owner

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?

@amitkot
Copy link

amitkot commented Oct 30, 2021

I want:

  • to use riverpod providers as dependencies when initializing prefs and auth.
  • to call async code for preparing prefs and auth.
  • to override them in the container
    as non-async providers (so that providers and other code that depend on them will not have to handle async complexities).

Since a container only allows updating of overrides, I must initially override prefs and auth even though I do not have values for them, and later when I do I can update the container with the override values.

@rrousselGit
Copy link
Owner

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),
]);

@rrousselGit
Copy link
Owner

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

@code-gezellig
Copy link

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 Future? Could you please share an example or update the documentation showing the actual concept instead of the old one you have on the website? https://riverpod.dev/docs/concepts/scopes#initialization-of-synchronous-provider-for-async-apis

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 enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants