Fix assertion failure when a provider depends on an ancestor of the same type (#796) - #923
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughProvider.of now uses an existing inherited element when listening, and new regression tests cover same-type ancestor reads for ChangesSame-type ancestor provider fix
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| // 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Description
Fixes #796.
When a provider both provides a value of type
Tand depends on an ancestor value of the same typeT, rebuilding it threw an assertion failure inside Flutter'sInheritedElement.notifyClients:This happens with, for example:
or the equivalent
ProxyProvider<AppTheme, AppTheme>.Root cause
Provider.of<T>resolves the ancestor_InheritedProviderScope<T?>element through the customgetElementForInheritedWidgetOfExactTypeoverride, which correctly skips the element itself when walking up the tree. However, it then registered the dependency with Flutter's standardcontext.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,
notifyClientsiterates its dependents and asserts each one is a descendant — the element is not a descendant of itself, so the assertion fails.Fix
Provider.ofnow depends on the resolved ancestor element directly viadependOnInheritedElement, mirroring the logic already used bycontext.select. It only falls back todependOnInheritedWidgetOfExactTypewhen no provider was found (preserving GlobalKey relocation support).Tests
Added two regression tests in
proxy_provider_test.dartcovering bothProxyProvider0<T>+Provider.of<T>andProxyProvider<T, T>. Both fail onmasterwith the original assertion and pass with this change.Summary by CodeRabbit