Skip to content

fix: make derived notifiers detach on last listener removals - #2

Closed
Feichtmeier wants to merge 2 commits into
flutter-it:mainfrom
Feichtmeier:fix/derived_notifier_detach
Closed

Feichtmeier wants to merge 2 commits into
flutter-it:mainfrom
Feichtmeier:fix/derived_notifier_detach

Conversation

@Feichtmeier

Copy link
Copy Markdown
  • Behavior change (fixes a listener leak): derived notifiers created by the operators (select, map, where, debounce, async, combineLatest*, mergeWith) now detach from their source when their last listener is removed, symmetric to how they attach on the first listener. Previously a chain stayed subscribed to its source forever (until dispose()), which leaked a listener on the source every time an operator chain was created inline and discarded (e.g. inside a build() / watchValue).
    • On re-observation a chain resyncs its value from the current source value, so resubscription remains correct and never stale.
    • The only observable difference: while a chain has zero listeners it no longer tracks its source (its .value is refreshed on the next observation instead of continuously). Chains that always have at least one listener are unaffected.

- Behavior change (fixes a listener leak): derived notifiers created by the
operators (`select`, `map`, `where`, `debounce`, `async`, `combineLatest*`,
  `mergeWith`) now **detach from their source when their last listener is
  removed**, symmetric to how they attach on the first listener. Previously a
  chain stayed subscribed to its source forever (until `dispose()`), which
  leaked a listener on the source every time an operator chain was created
  inline and discarded (e.g. inside a `build()` / `watchValue`).
  - On re-observation a chain **resyncs** its value from the current source
    value, so resubscription remains correct and never stale.
  - The only observable difference: while a chain has **zero** listeners it no
    longer tracks its source (its `.value` is refreshed on the next
    observation instead of continuously). Chains that always have at least one
    listener are unaffected.
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

@Feichtmeier
Feichtmeier force-pushed the fix/derived_notifier_detach branch from b4864d7 to 9232d68 Compare July 27, 2026 11:13
@Feichtmeier
Feichtmeier force-pushed the fix/derived_notifier_detach branch from 9232d68 to b2a5490 Compare July 27, 2026 11:34
@Feichtmeier
Feichtmeier marked this pull request as ready for review July 27, 2026 12:28
escamoteur added a commit that referenced this pull request Sep 19, 2026
…e on read

Chains now remove their handler from the source when their last listener is
removed and re-attach (with resync) on the next one. While detached, .value is
derived from the current source value so it never goes stale (the
functional_listener escamoteur#13 scenario). Fixes listener accumulation on long-lived
sources when chains are created per widget instance. Based on #2 by
@Feichtmeier. Also fixes AsyncValueNotifier writing into a disposed notifier,
deduplicates the combining notifiers, updates README/docs/skill, removes
cross-package skill copies and stops committing pubspec.lock.
@escamoteur

Copy link
Copy Markdown
Member

Thank you — this was a real problem (one dead chain per widget instance accumulating on long-lived sources) and your detach mechanism is the core of what ships in 6.0.0. We couldn't take the PR as-is, though: detach-on-last-listener is exactly what functional_listener had until v4.0.0, where it was removed because .value went stale while a chain had no listeners (escamoteur#13). So 6.0.0 combines your detach/re-attach with "derive on read": while detached, .value is computed from the current source value (map → transform(source.value), combineLatest → combiner(...), etc.; only mergeWith keeps its last value). The escamoteur#13 scenario works again, and your musicpod case no longer leaks. Credited you in the changelog. Also: .value of a detached chain runs the transform on read, so transforms should stay pure (they should anyway).

@escamoteur escamoteur closed this Sep 19, 2026
@Feichtmeier

Copy link
Copy Markdown
Author

thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants