In Flutter's provider package, when does a provider's create callback actually run, and what does passing lazy: false change?
answer
- not when inserted in the tree
- first read triggers create
- lazy: false computes during build
- unread providers never create
- a throwing create is remembered
basics
~20 sA provider's create runs lazily, the first time a descendant reads the value, not when the provider is inserted. lazy: false makes the provider compute its value when it builds, so creation side effects start even if nothing has read it yet.
solid answer
~40 sIn the provider package `create` is called lazily by default: inserting `Provider<BakeryApi>(create: ...)` into the tree does nothing until some descendant reads `BakeryApi`, and then `create` runs exactly once. Proxy providers' `update` is lazy in the same way. Laziness saves work for values a screen may never need, but it also means side effects in `create` — starting to load the menu, opening a connection — are delayed or never happen if nothing reads the value. Passing `lazy: false` makes the provider read its own value while building, so creation happens eagerly, for example to begin fetching today's specials during start-up. `FutureProvider.value` and `StreamProvider.value` always start eagerly. If `create` throws, the first read rethrows the error, and later reads throw a `StateError` saying the provider threw during creation instead of calling `create` again.
code
dart · 31 linesimport 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MenuNotifier extends ChangeNotifier {
List<String> items = const [];
bool loading = false;
Future<void> load() async {
loading = true;
notifyListeners();
await Future<void>.delayed(const Duration(milliseconds: 300));
items = const ['Sourdough', 'Cinnamon roll'];
loading = false;
notifyListeners();
}
}
void main() {
runApp(
MultiProvider(
providers: [
// Eager: starts loading the menu while the splash screen shows.
ChangeNotifierProvider<MenuNotifier>(
create: (_) => MenuNotifier()..load(),
lazy: false,
),
],
child: const MaterialApp(home: Scaffold(body: Text('Loading...'))),
),
);
}go deeper
Know that create is called on the first read, not when the provider is added, and that lazy: false makes it run immediately.
Explain why laziness saves start-up work, how it delays side effects in create, and what later reads do after create has thrown.
Decide per provider whether eager creation is justified, keep constructors cheap and non-throwing, and avoid relying on create side effects for app correctness.
Treat root-level provider creation as a start-up budget: which values warm eagerly, which stay lazy, and how failures surface early enough to diagnose.
## Laziness is the default The provider package's documentation for `Provider` says it plainly: the `create` callback is called **the first time the value is read**, not the first time the provider is inserted in the widget tree. The same holds for the `create` and `update` callbacks of the other providers. In the source, a provider's element computes its value on the first access and only forces that access during `build` when `lazy` is explicitly `false`. This matters because a provider placed high in the tree — above `MaterialApp`, typically inside a `MultiProvider` — is inserted as soon as the app starts, but many of its values are used by only one screen. | Setting | When `create` runs | Typical use | |---|---|---| | default (`lazy` omitted) | first read by a descendant | most services and notifiers | | `lazy: false` | when the provider builds | values whose creation must start work early | | `.value` constructors | never; the value already exists | exposing existing objects | ## Benefits of the default - **Start-up cost.** Dozens of providers at the root do not construct dozens of objects on the first frame; each is built when a screen first needs it. - **Unused features cost nothing.** A `ChangeNotifierProvider<LoyaltyCard>` for a rarely opened screen never allocates its notifier unless the user goes there. - **Order-independent creation.** Because values are created on demand, a provider can read another in its `create` without caring which was constructed first, as long as the other is above it. ## When laziness surprises people 1. **Side effects in `create` are delayed.** A `ChangeNotifierProvider(create: (_) => MenuNotifier()..load())` does not start loading the menu until something reads `MenuNotifier`. If the goal is to prefetch during a splash screen, nothing happens. 2. **Side effects may never happen.** A provider nobody reads never runs `create` at all — a logging or analytics-style object that is only created for its constructor's side effect silently does nothing. 3. **Errors surface late.** A misconfigured object throws at the first read, possibly deep inside a later screen, rather than at start-up. `lazy: false` addresses all three: the provider forces its own value while building, so `create` runs right away. ## What happens when `create` throws The provider remembers the failure. The first read rethrows the original exception. Later reads do **not** call `create` again; they throw a `StateError` explaining that the provider threw during the creation of its value, with the original error attached (provider 6.0.2 added those details). So a flaky initialisation is not retried by reading again; it needs the provider to be recreated, for example by rebuilding the subtree with a new key, or better, by moving fallible work out of the constructor and into a method that reports its own error state. ## Async providers `FutureProvider` and `StreamProvider` with `create` follow the same laziness: the future is not requested until the value is read, and until it completes readers see `initialData`. Their `.value` constructors pass `lazy: false` internally, so an existing future or stream is listened to as soon as the provider builds. ## Laziness and ordering inside `MultiProvider` Because creation is deferred to the first read, the order of providers in a `MultiProvider` list affects visibility, not construction order. A `ChangeNotifierProvider<Cart>` listed after `Provider<PriceList>` may read the price list in its `create`; the price list is created at that moment if nothing has read it yet. The reverse direction does not work: a provider can only read providers above it, so listing `Cart` first and reading `PriceList` from its `create` fails with a lookup error regardless of laziness. With `lazy: false`, construction follows the build order of the tree, which is the list order. ## Guidelines - Keep the default for most providers. - Use `lazy: false` for values whose creation starts work the app needs soon, such as warming a cache during a splash screen. - Do not rely on `create` side effects for correctness; if something must happen at start-up, make it explicit. - Keep constructors cheap and non-throwing, so laziness never hides an initialisation failure.
- Does laziness apply to ProxyProvider's update callback too?Yes. A proxy provider's `update` is first called when its value is first read, alongside `create` if one is given. After that it runs again when the proxy rebuilds or when a provider it depends on changes. The README notes that both callbacks are lazy by default, and `lazy: false` makes the first call happen when the proxy builds.
- Why is lazy: false not simply the better default?Because it constructs every value at start-up whether or not any screen uses it. With many providers at the root, that adds work to the first frame and allocates objects for features the user may never open. Eager creation is worth it only for values whose creation must start something early.
saying these in an interview costs you the question
- create runs as soon as the provider is inserted into the widget tree.
- lazy: false turns off disposal of the created object.
- If create throws, the next read simply calls create again.
- A provider that nothing reads still runs create once at start-up.
- lazy: false makes create run again on every rebuild.