Skip to content

Fix assertion failure when a provider depends on an ancestor of the same type (#796) - #923

Open
naghinezhad wants to merge 1 commit into
rrousselGit:masterfrom
naghinezhad:fix/796-proxyprovider-self-dependency
Open

Fix assertion failure when a provider depends on an ancestor of the same type (#796)#923
naghinezhad wants to merge 1 commit into
rrousselGit:masterfrom
naghinezhad:fix/796-proxyprovider-self-dependency

Conversation

@naghinezhad

@naghinezhad naghinezhad commented Jun 25, 2026

Copy link
Copy Markdown

Description

Fixes #796.

When a provider both provides a value of type T and depends on an ancestor value of the same type T, rebuilding it threw an assertion failure inside Flutter's InheritedElement.notifyClients:

'package:flutter/src/widgets/framework.dart': Failed assertion: ... // check that it really is our descendant

This happens with, for example:

Provider<AppTheme>(
  create: (_) => AppTheme(),
  child: ProxyProvider0<AppTheme>(
    update: (context, _) => PartialAppTheme(Provider.of<AppTheme>(context)),
    child: ...,
  ),
);

or the equivalent ProxyProvider<AppTheme, AppTheme>.

Root cause

Provider.of<T> resolves the ancestor _InheritedProviderScope<T?> element through the custom getElementForInheritedWidgetOfExactType override, which correctly skips the element itself when walking up the tree. However, it then registered the dependency with Flutter's standard context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>(), which resolves to the nearest provider of that type.

When the call originates from inside a provider that exposes the same type it consumes, that nearest provider is the element itself, so the element registered a dependency on itself. On the next rebuild, notifyClients iterates its dependents and asserts each one is a descendant — the element is not a descendant of itself, so the assertion fails.

Fix

Provider.of now depends on the resolved ancestor element directly via dependOnInheritedElement, mirroring the logic already used by context.select. It only falls back to dependOnInheritedWidgetOfExactType when no provider was found (preserving GlobalKey relocation support).

Tests

Added two regression tests in proxy_provider_test.dart covering both ProxyProvider0<T> + Provider.of<T> and ProxyProvider<T, T>. Both fail on master with the original assertion and pass with this change.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an assertion issue that could occur when a provider depends on an ancestor provider of the same type.
    • Improved rebuild handling so values update correctly when providers move or are rebuilt.
    • Added regression coverage to ensure related provider setups render updated content without errors.

…ame type

`Provider.of<T>` resolved the ancestor `_InheritedProviderScope<T?>` element via
the self-skipping `getElementForInheritedWidgetOfExactType` override, but then
registered the dependency through Flutter's standard
`dependOnInheritedWidgetOfExactType`, which resolves to the *nearest* provider of
that type. When called from inside a provider that provides the same type it
depends on (e.g. `ProxyProvider<T, T>`, or `context.watch<T>()` inside a
`ProxyProvider0<T>`'s `update`), that nearest provider is the element itself, so
it ended up depending on itself. On rebuild this tripped the descendant check in
`InheritedElement.notifyClients`.

Now `Provider.of` depends on the resolved ancestor element directly via
`dependOnInheritedElement`, mirroring the existing logic in `select`, and only
falls back to `dependOnInheritedWidgetOfExactType` when no provider was found
(to preserve GlobalKey relocation support).

Fixes rrousselGit#796

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 346b3652-6f5d-43d5-bd6a-3de9fbb67d6b

📥 Commits

Reviewing files that changed from the base of the PR and between 6cfb56a and 63f0304.

📒 Files selected for processing (3)
  • packages/provider/CHANGELOG.md
  • packages/provider/lib/src/provider.dart
  • packages/provider/test/null_safe/proxy_provider_test.dart

📝 Walkthrough

Walkthrough

Provider.of now uses an existing inherited element when listening, and new regression tests cover same-type ancestor reads for ProxyProvider0<String> and ProxyProvider<String, String>; the changelog records the fix.

Changes

Same-type ancestor provider fix

Layer / File(s) Summary
Inherited lookup path
packages/provider/lib/src/provider.dart
Provider.of<T>(context, listen: true) now uses dependOnInheritedElement when an inherited element exists and otherwise falls back to dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>().
Regression tests and changelog
packages/provider/test/null_safe/proxy_provider_test.dart, packages/provider/CHANGELOG.md
Regression tests cover ProxyProvider0<String> and ProxyProvider<String, String> reading ancestor Provider<String> values, and the changelog adds the unreleased fix note.

Sequence Diagram(s)

sequenceDiagram
  participant Update as "ProxyProvider.update"
  participant ProviderOf as "Provider.of<T>(context, listen: true)"
  participant Context as BuildContext

  Update->>ProviderOf: read ancestor T
  ProviderOf->>Context: check inheritedElement
  alt inheritedElement exists
    ProviderOf->>Context: dependOnInheritedElement(inheritedElement)
  else no inheritedElement
    ProviderOf->>Context: dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>()
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through the provider mist,
Found one parent none could resist.
No self-made loop, just base-b hops,
And rebuilds bounced on leafy tops.
Hooray! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main fix and references the affected scenario and issue.
Linked Issues check ✅ Passed The code and tests address #796 by allowing a provider to read an ancestor of the same type without self-dependency.
Out of Scope Changes check ✅ Passed The changelog update and tests are directly related to the fix, with no unrelated changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

// when a provider depends on an ancestor provider of the same type
// (such as a ProxyProvider<T, T>). Depending on oneself triggers an
// assertion failure when the dependents are later notified.
context.dependOnInheritedElement(inheritedElement);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said in the old comment, this doesn't work with GlobalKeys. (although maybe that's fixed now)

Afaik providers already skip themselves. I remember writing explicit logic calling element.visitAncestors to have providers skip themselves. So the comment does not make sense to me. Maybe the issue is somewhere else or I misremember

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick review! Both points are fair — let me address them with what I found.

"Providers already skip themselves"

They do, but only for the value lookup, not for the dependency registration — and that asymmetry is exactly the bug.

The skip-self logic you're remembering is the visitAncestorElements override of getElementForInheritedWidgetOfExactType in inherited_provider.dart. Provider.of uses it (via _inheritedElementOf) to read the value, so the value is correctly resolved to the ancestor.

But the listen branch then registered the dependency through context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>(). That method is not overridden, so it uses the framework default, which looks the type up in Element._inheritedElements. For an InheritedElement, _updateInheritance() inserts itself into its own _inheritedElements map, so the lookup resolves to the element itself — and the provider ends up registered as a dependent of itself.

So when a provider provides T and also depends on an ancestor T:

  • value → ancestor ✅ (skips self)
  • dependency → self ❌ (does not skip self)

On the next rebuild, notifyClients walks its dependents and asserts each is a descendant; the element is not a descendant of itself, which is the assertion in the issue. You can see it directly in the original error: the failing element is an _InheritedProviderScope<AppTheme?> whose dependencies: [_InheritedProviderScope<AppTheme?>] — i.e. it depends on its own type.

The fix just makes the dependency path agree with the value path: depend on the already-resolved ancestor element via dependOnInheritedElement(inheritedElement). This is the same pattern select already uses a few lines above.

GlobalKeys

The fix keeps dependOnInheritedWidgetOfExactType for exactly the inheritedElement == null case (no provider found), which is the only case the old comment was about ("...if no provider were found previously"). The GlobalKey-relocation-from-no-provider path is unchanged.

I verified this: both context_test.dart tests named "supports relocating with GlobalKey from no provider to a provider" still pass with this change (the whole context_test.dart suite is green). When a provider is found, depending on it directly is fine, since a relocation re-runs build/Provider.of and re-resolves the ancestor.

Happy to adjust the code comment if the current wording is unclear, or to take a different approach (e.g. overriding dependOnInheritedWidgetOfExactType to skip self) if you'd prefer that.

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.

ProxyProvider triggers an assertion failure when depending on a provided value of the same type it is providing

2 participants