With the provider package, when does a provider dispose its value, and why is an instance passed to a .value constructor never disposed?
answer
- ownership decides disposal
- dispose runs on unmount
- ChangeNotifierProvider calls notifier.dispose
- Dispose<T> gets context and value
- .value only removes its listener
basics
~20 sA provider disposes only what its create callback built, when the provider widget is unmounted: ChangeNotifierProvider calls dispose() itself, Provider calls its dispose callback. A .value constructor exposes an instance someone else owns, so it never disposes it.
solid answer
~40 sDisposal in provider follows **ownership**. With the default constructor the provider built the object in `create`, so when the provider widget leaves the tree it cleans up: `ChangeNotifierProvider` calls `notifier.dispose()` automatically, and `Provider` and `ListenableProvider` call the `dispose: (context, value) => ...` callback you pass. If `create` never ran, because the value was lazy and never read, there is nothing to dispose. A `.value` constructor exposes an instance created elsewhere, so on unmount it only removes its listener; the real owner, typically a `State` that disposes it in `State.dispose`, must clean up. Mixing these up gives two bugs: `create: (_) => existing` disposes an object still in use, and `.value(value: Model())` in `build` leaks a fresh instance on every rebuild.
code
dart · 27 linesimport 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AnswerStream {
final StreamController<String> _controller = StreamController<String>.broadcast();
Stream<String> get answers => _controller.stream;
void add(String answer) => _controller.add(answer);
Future<void> close() => _controller.close();
}
class SurveyScope extends StatelessWidget {
const SurveyScope({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Provider<AnswerStream>(
create: (_) => AnswerStream(),
// called on unmount, only if the value was ever created
dispose: (_, stream) => stream.close(),
child: child,
);
}
}go deeper
Recall that ChangeNotifierProvider with create disposes the notifier for you, while .value never does.
Explain ownership: create means the provider owns and disposes on unmount, .value means someone else owns it, plus the lazy case where nothing is disposed.
Diagnose used-after-dispose assertions and leaks by tracing which provider claimed ownership, and place resource cleanup in dispose callbacks or the notifier's own dispose.
Define an ownership rule for shared objects across a codebase so every long-lived resource has exactly one disposer, reviewed at the provider boundary.
## Ownership decides disposal The provider package ties an object's cleanup to **who created it**. A provider that built the object through its `create` callback owns it and disposes it when the provider widget is **unmounted**, which happens when a route pops, a conditional branch stops returning the provider, or a key change replaces it. A provider built with a `.value` constructor received an object from outside, so it never disposes it. | Constructor | Who creates the object | On unmount | |---|---|---| | `Provider(create: ..., dispose: ...)` | the provider | calls your `dispose(context, value)` callback, if given | | `ChangeNotifierProvider(create: ...)` | the provider | calls `notifier.dispose()` automatically | | `ListenableProvider(create: ..., dispose: ...)` | the provider | removes its listener, then calls your `dispose` callback, if given | | any `.value(value: ...)` | the caller | removes its listener only; never disposes | The callback type is `Dispose<T>`, a `void Function(BuildContext context, T value)`. `ChangeNotifierProvider` has no `dispose` parameter because it always supplies one that calls `ChangeNotifier.dispose()`. ## Lazy creation and disposal Providers create lazily by default: `create` runs the first time the value is read, not when the provider mounts. The package only calls `dispose` if a value was actually created, so a provider whose value was never read disposes nothing. That matters when `dispose` has side effects, such as flushing a draft to disk: if nobody read the provider, the callback never ran. Passing `lazy: false` creates the value on mount instead. ## Why .value never disposes A `.value` provider is a window onto an object whose lifetime is managed elsewhere, for example a `State` that owns a controller, or a parent provider re-exposed on a pushed route. If it disposed on unmount, any short-lived `.value` wrapper (a dialog, a pushed page) would destroy an object its real owner still uses. So the owner must dispose it: 1. create the object in `initState` of a `State` (or in a parent provider's `create`); 2. expose it with a `.value` constructor where needed; 3. dispose it in `State.dispose` (or let the parent provider do it). ## Owning an object in a State When a widget must own an object itself (for example because it needs the object in `initState`), the pattern is: ```dart class _SurveyHostState extends State<SurveyHost> { late final SurveyAnswers _answers = SurveyAnswers(); @override void dispose() { _answers.dispose(); // the owner disposes super.dispose(); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _answers, // exposes, never disposes child: widget.child, ); } } ``` The instance is created once per `State`, survives rebuilds, and has exactly one disposer. ## The two ownership bugs - **`create` returning an existing object.** `ChangeNotifierProvider(create: (_) => existingNotifier, ...)` claims ownership of something it did not create. When this provider unmounts it calls `dispose()`, and the next use by the real owner hits Flutter's debug assertion 'A X was used after being disposed.' The package documentation warns that this 'may dispose the ChangeNotifier when it is still in use'. - **`.value` with a new object in `build`.** `ChangeNotifierProvider.value(value: SurveyAnswers(), ...)` creates a fresh instance every time `build` runs and never disposes any of them. The docs call this a path to 'memory leaks and potentially undesired side-effects', and state resets on every rebuild. ## Disposing more than notifiers The `dispose` callback is how plain `Provider` cleans up non-listenable resources: - closing a `StreamController` or a socket wrapper; - cancelling a timer inside a service object; - closing a database handle opened for one screen. For a `ChangeNotifier`, put that cleanup inside the notifier's own `dispose()` override; `ChangeNotifierProvider` will call it. ## Checking your understanding - A screen-scoped provider disposes its value when the screen's route is popped. - An app-wide provider above `MaterialApp` effectively never disposes during normal use, so its objects must not hold per-screen resources. - A `.value` provider can be unmounted any number of times without affecting the object. - Disposal is synchronous during unmount; do not start async work in `dispose` that expects the widget tree to still be there.
- A Provider's dispose callback flushes a draft, but sometimes the draft is never saved. What could explain it?Providers create lazily, and provider only calls `dispose` if `create` actually ran. If no widget read the value before the provider unmounted, there was no value and no callback. Either read it earlier, pass `lazy: false`, or move the flush to where the draft changes.
- Who should dispose a ChangeNotifier created in a State's initState and exposed with ChangeNotifierProvider.value?The `State` that created it, in its `dispose` method. The `.value` provider only removes its listener when unmounted, so without the `State` disposing it the notifier and its listeners leak.
A provider's create is like renting equipment in your own name: when you leave, you return it. A .value provider is borrowing a colleague's tool for an afternoon: when you leave, you hand it back and the colleague decides when it is thrown away.
saying these in an interview costs you the question
- ChangeNotifierProvider.value disposes the notifier when the route pops
- Provider's dispose callback runs even if the value was never created
- create: (_) => existingNotifier is a safe way to share an instance
- Creating the notifier inside .value in build is fine because build is cheap
- An app-wide provider disposes its value whenever a screen closes