Eager initialization of providers
All providers are initialized lazily by default. This means that the provider is only initialized when it is first used. This is useful for providers that are only used in certain parts of the application.
Unfortunately, there is no way to flag a provider as needing to be eagerly initialized due to how Dart works (for tree shaking purposes). One solution howerver is to forcibly read the providers you want to eagerly initialize at the root of your application.
The recommended approach is to simply "watch" a provider in a Consumer placed right under your ProviderScope
:
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return const _EagerInitialization(
// TODO: Render your app here
child: MaterialApp(),
);
}
}
class _EagerInitialization extends ConsumerWidget {
const _EagerInitialization({required this.child});
final Widget child;
Widget build(BuildContext context, WidgetRef ref) {
// Eagerly initialize providers by watching them.
// By using "watch", the provider will stay alive and not be disposed.
ref.watch(myProvider);
return child;
}
}
Consider putting the initialization consumer in your "MyApp" or in a public widget. This enables your tests to use the same behavior, by removing logic from your main.
FAQ
Won't this rebuild our entire application when the provider changes?
No, this is not the case.
In the sample given above, the consumer responsible for eagerly initializing
is a separate widget, which does nothing but return a child
.
The key part is that it returns a child
, rather than instantiating MaterialApp
itself.
This means that if _EagerInitialization
ever rebuilds, the child
variable
will be not have changed. And when a widget doesn't change, Flutter doesn't rebuild it.
As such, _EagerInitialization
will rebuild but nothing else (unless another widget
listens to that provider, of course).
Using this approach, how can I handle loading and error states?
You can handle loading/error states as you normally would in a Consumer
.
Your _EagerInitialization
could check if a provider is in "loading" state,
and if so, return a CircularProgressIndicator
instead of the child
:
class _EagerInitialization extends ConsumerWidget {
const _EagerInitialization({required this.child});
final Widget child;
Widget build(BuildContext context, WidgetRef ref) {
final result = ref.watch(myProvider);
// Handle error states and loading states
if (result.isLoading) {
return const CircularProgressIndicator();
} else if (result.hasError) {
return const Text('Oopsy!');
}
return child;
}
}
I've handled loading/error states, but other Consumers still receive an AsyncValue! Is there a way to not have to handle loading/error states in every widget?
Rather than trying to have your provider not expose an AsyncValue
, you can
instead have your widgets use AsyncValue.requireValue
.
This will read the data without having to do pattern matching. And in case a bug slips through,
it will throw an exception with a clear message.
// An eagerly initialized provider.
Future<String> example(ExampleRef ref) async => 'Hello world';
class MyConsumer extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final result = ref.watch(exampleProvider);
/// If the provider was correctly eagerly initialized, then we can
/// directly read the data with "requireValue".
return Text(result.requireValue);
}
}
Although there are ways to not expose the loading/error states in those cases (relying on scoping),
it is generally discouraged to do so.
The added complexity of making two providers and using overrides is not worth the trouble.