skip to content

With the provider package, when does a provider dispose its value, and why is an instance passed to a .value constructor never disposed?

level: middleimportance: should knowfreq 42%

answer

  1. ownership decides disposal
  2. dispose runs on unmount
  3. ChangeNotifierProvider calls notifier.dispose
  4. Dispose<T> gets context and value
  5. .value only removes its listener

basics

~20 s

A 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 s

Disposal 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 lines
dart
import '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

for a junior

Recall that ChangeNotifierProvider with create disposes the notifier for you, while .value never does.

for a middle

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.

for a senior

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.

for a principal

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