-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Inheritedwidget update #107112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inheritedwidget update #107112
Conversation
Also allows Elements to eagerly remove their dependencies fixes flutter#106549, fixes flutter#106546
|
For reference, this would later be used by Riverpod as followed (quick code just for demonstration): import 'package:flutter/widgets.dart';
import 'internals.dart';
class ProviderContext extends InheritedWidget {
const ProviderContext({super.key, required super.child});
@override
ProviderContextElement createElement() => ProviderContextElement(this);
@override
bool updateShouldNotify(covariant ProviderContext oldWidget) {
return false;
}
}
class ProviderContextElement extends InheritedElement {
ProviderContextElement(super.widget);
@override
bool get clearDependencyOnRebuild => true;
final _container = ProviderContainer();
@override
Map<ProviderListenable, ProviderSubscription<Object?>>? getDependencies(
Element dependent) {
return super.getDependencies(dependent)
as Map<ProviderListenable, ProviderSubscription<Object?>>?;
}
T _watch<T>(ProviderListenable<T> provider, Element dependent) {
dependent.dependOnInheritedElement(this, aspect: provider);
return getDependencies(dependent)![provider]!.read() as T;
}
@override
@protected
void updateDependencies(
Element dependent,
covariant ProviderListenable<Object?> key,
) {
var deps = getDependencies(dependent);
if (deps == null) {
deps = {};
setDependencies(dependent, deps);
}
deps.putIfAbsent(key, () {
void listener(Object? prev, Object? next) => dependent.markNeedsBuild();
final sub = _container.listen<Object?>(key, listener);
return sub;
});
}
@override
@protected
void removeDependencies(Element dependent) {
final subs = getDependencies(dependent);
if (subs != null) {
super.removeDependencies(dependent);
for (final sub in subs.values) {
sub.close();
}
}
}
@override
void unmount() {
_container.dispose();
super.unmount();
}
}
///
extension RiverpodContext on BuildContext {
T watch<T>(ProviderListenable<T> provider) {
final element = getElementForInheritedWidgetOfExactType<ProviderContext>()!
as ProviderContextElement;
return element._watch(provider, this as Element);
}
T read<T>(ProviderBase<T> provider) {
final element = getElementForInheritedWidgetOfExactType<ProviderContext>()!
as ProviderContextElement;
return element._container.read(provider);
}
}I have tested this code and it achieves the desired behavior, which is preserving Riverpod features while supporting raw StatelessWidgets (instead of having to use a custom subclass of Widget) |
|
This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again. For more guidance, visit Writing a golden file test for Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
| /// * [InheritedModel], which is an example of a class that uses this method | ||
| /// to manage dependency values. | ||
| @protected | ||
| void removeDependencies(Element dependent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mustCallSuper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at it, the other methods do not have such annotation. So not sure if it really makes sense
| @override | ||
| InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) { | ||
| assert(ancestor != null); | ||
| _hasDependencyWhichNeedsClearingOnRebuild = _hasDependencyWhichNeedsClearingOnRebuild || ancestor.clearDependencyOnRebuild; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the _hasDependencyWhichNeedsClearingOnRebuild flag reset when the dependency on ancestor is cleared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I forgot this. Will add this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Although thinking about it, this does not have any behavioral impact. The only impact would be performance
| /// conditionally depend on an [InheritedElement]. | ||
| /// | ||
| /// This value should never change for the lifetime of the element. | ||
| bool get clearDependencyOnRebuild => false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@protected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not internal to InheritedElement though. It's used by other types of Elements too. So is it really "protected"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protected is defined as:
Used to annotate an instance member in a class or mixin which is meant to be visible only within the declaring library, and to other instance members of the class or mixin, and their subtypes.
Isn't that exactly what we want here?
|
|
||
| final Map<Element, Object?> _dependents = HashMap<Element, Object?>(); | ||
|
|
||
| /// Whether dependent widgets should unsubscribe to this [InheritedElement] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like dangerous API. If the dependent established the dependency in didChangeDependencies the dependency will unexpectedly be cleared (and not re-established) on any dependency-unrelated rebuild. The dependent will then miss future dependency updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The "should" here is misleading, dependents don't get a choice, their dependency is forcefully cleared on rebuild when this is set.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is meant to be used for build tacking. Using it inside didChangeDependencies doesn't make sense.
We could have an assert to prevent such usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added an assert that only allows adding a dependency on such InheritedElement inside build.
Using it in initState/didChangeDependencies/... will throw
I added an exception case for LayoutBuilders too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is meant to be used for build tacking. Using it inside didChangeDependencies doesn't make sense.
This should be documented a part of the doc comment.
|
Hello @goderbauer! I'd like to know if I can confidently assume that this will be merged at some point, as this change is pivotal for Riverpod. |
Since InheritedWidgets are a very basic building block of the Flutter framework, I'd like to get the opinion from other team members on this one as well before making a decision. I'll get back to you. |
| assert( | ||
| !ancestor.clearDependencyOnRebuild || debugDoingBuild || !dirty, | ||
| 'Cannot depend on an InheritedElement with clearDependencyOnRebuild: true ' | ||
| ' in Widget life-cycles other than `build`.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove the space before "in" to avoid a double space in the string (maybe even just put the entire sting on one line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the error you get when trying to establish a dependency in initState, this should probably throw a more detailed FlutterError to guide the developer.
| InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) { | ||
| assert(ancestor != null); | ||
| assert( | ||
| !ancestor.clearDependencyOnRebuild || debugDoingBuild || !dirty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the !dirty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's meant to disable the assert outside of the widget life-cycles (so after build has completed)
That enables support for things like using context. dependOnInheritedElement inside ListView.builder/LayoutBuilder – as they have this "layout on depend" which happens after the widget's build method has completed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add that explanation as a comment.
However, this will now also allow developers to call this in other handlers outside of build, e.g. a pointer event handler, no? It may actually be pretty confusing there. If it is called from an event handler while the widget is clean, it goes through without error. If it is called from the same event handler after the widget has already been marked as dirty otherwise, it will fail.
| /// conditionally depend on an [InheritedElement]. | ||
| /// | ||
| /// This value should never change for the lifetime of the element. | ||
| bool get clearDependencyOnRebuild => false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protected is defined as:
Used to annotate an instance member in a class or mixin which is meant to be visible only within the declaring library, and to other instance members of the class or mixin, and their subtypes.
Isn't that exactly what we want here?
|
|
||
| final Map<Element, Object?> _dependents = HashMap<Element, Object?>(); | ||
|
|
||
| /// Whether dependent widgets should unsubscribe to this [InheritedElement] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is meant to be used for build tacking. Using it inside didChangeDependencies doesn't make sense.
This should be documented a part of the doc comment.
| /// dependencies value to decide if the dependent needs to be rebuilt. | ||
| /// * [InheritedModel], which is an example of a class that uses this method | ||
| /// to manage dependency values. | ||
| @protected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at it, the other methods do not have such annotation. So not sure if it really makes sense
What other methods do you mean?
I would argue that this one needs it because if you forget it you actually have no way of fulfilling the contract of the method since _dependents is private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getDependencies/setDependencies
They both refer to _dependents too, but don't have mustCallSuper
| /// the widget rebuilds. | ||
| /// This only works if we assume that [InheritedElement.clearDependencyOnRebuild] | ||
| /// never changes. | ||
| bool _hasDependencyWhichNeedsClearingOnRebuild = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am actually wondering: Would it simplify things if we just track the dependencies that we need to clear in a separate list instead of having this flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be a lot less efficient though, wouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's slower to push into a list than to do a simple boolean operation.
And I'll admit, I don't see why a list would simplify the logic.
Thanks! |
|
For some reason, github doesn't allow me to answer your comment in the comment section, so copy-pasting it here:
My comment refers to how the current use-case isn't a subtype, but rather a base type. Instead of Element exposing this property, then InheritedElement using it, it's the opposite: InheritedElement defines it, and Element uses it. But it's probably not worth spending too much time on this. I'll add the annotation |
|
After thinking about this some more: This PR basically implements two separate features:
Are these features only useful for you as a pair? The changes for 1) are pretty uncontroversial. So, if those alone are useful to you, you could split those out in a separate PR and we can get that one approved pretty quickly. The second feature is a little more controversial as it fundamentally changes the contract around inherited widgets, which creates some usability concerns (can't be used from |
|
I need them in pair, yes. About But I'll admit, I don't care about |
|
In a world where I update this PR to support I wouldn't want to spend too much time on supporting that if that changes nothing to the outcome though. |
38fad60 to
44ffa02
Compare
|
As someone that has implemented a custom InheritedElement before, this change is a nice way to know when dependents stop listening. Without it, we have no way of cleaning up resources or stop notifying dependents that are no longer using our InheritedWiget during build. While this solution doesn't result in a simple and easy to use API, it seems like the only way to do it without also regressing performance in the average use case of InheritedWiget. |
|
After some more deliberation I still don't think we should do this. Also, the use case that was mentioned somewhere of disposing resources when nobody is listening anymore can be implemented on top of the existing mechanism: Instead of vending the resource directly from the |
An object with a dispose function is fundamentally incompatible with this proposal. The core goal behind this API is to simplify the logic in consumers as much as possible.
Could you clarify? I don't understand how that's different from existing InheritedWidgets.
As I mentioned before, I'm fairly sure we could get |
The InheritedWidget decides whether a dependency is cleared or not, that's different from how InheritedWidget behaves today. |
The if you want, we could add an assert in the call site of Maybe something among the lines of: element.removeDependent(this);
assert(!element._dependents.contains(this), 'Bad state, the dependentwas not removed'); |
|
I was talking about the As I said before, I am totally fine with exposing |
|
An alternate proposal I made was to add the logic on What about: I implemented this PR this way because having the logic on the InheritedElement means no newcomer would come accross this logic. |
|
I also acknowledge that the problem of not clearing dependencies is a real issue and one that we should ideally fix. For the reasons outlined in my other comments, I don't think this here is the right fix. |
|
The library has to adapt to Flutter and not the other way around. I find this approach very dangerous for the reasons cited above. I believe the Flutter team will make the best decision, but I don't agree with this PR. |
|
I dont think it's good idea the flutterx change to adapt to this riverpod! Changing the tree this way it will cause the same problem the dom of browsers had had in 2010 when the dom was unlock to be manipulated direclly with whatever we want and the result before was crashes, unexpected render bugs, poor performance. To solve this the tools like react virtual doom came with reconciliation algorithm to solve a problem we wont have if the architetural limit had have mantained. For me this case it's the same. Today it's this riverpod, tomorrow it's another package ou another over engenering to solve futures problems into core elements of flutter wich the cause was this kind modifications. |
|
The problem is by no means specific to Riverpod. I explained it that way, because that's the most specific reason why I need this change. To avoid the "x/y problem". The ecosystem will benefit it as a whole, such as SharedAppData being enabled to do more. There's a design doc will cover that in due time. I'll close this PR while I'm working on the docs |
|
Context is Flutter's spine and it should be safe-guarded from devs (we've seen what devs can do when they started manipulating DOM as @pauloantonelli mentioned). So, my question is: once we unlock this door, can we GUARANTEE that devs won't be able to f* up the Context integrity? I won't be surprised if in six months from now we start seeing apps crashing "randomly" just because some nice package is messing around with the context in a way that "nobody haven't predicted"! Context should not be allowed to be changed as simple as that. Actually, Flutter should not be changed as simple as that just to adapt to a package needs. I think it should be the opposite. Unfortunately, I'm not able to see the "ecosystem benefits" as you mentioned (perhaps we need more clarifications). What I see instead is a breach that will let devs do things that they shouldn't be doing. I spend hours and hours on the discord helping dozens of devs a day "fixing" problems they wouldn't had if they just stop using these "silver-bullet" packages. I teach them to use what we get natively because it is so SIMPLE to use. I see a lot of packages trying to change the way Flutter was originally designed to work, forcing some architectures with a bunch of unnecessary overengineering, when at the end of the day ALL can be done in an elegant way, just by using what Flutter's creators designed originally: setState/changeNotifier/inheritedWidget. (Please let me highlight that is just my opinion and my opinion only.) Well, leaving my personal opinion aside, I think these changes should not be applied until we have more deep discussions with the community, 'cos we really need to be aware of all its benefits and side effects. |
|
I don't understand what's up with y'all. This PR is about fixing a Flutter "bug" that's been around for a long time (widgets never stopping to listen to an InheritedWidget). The team itself agreed that fixing this bug would be valuable (#107112 (comment)) |
|
Correct me if I'm wrong, but I believe those comments are coming from the following video: https://www.youtube.com/watch?v=UQROTqpX0RM It appears that the way the video described this PR is incorrect. It's about fixing an issue where if a widget does: Widget build(context) {
if (condition) {
Theme.of(context);
}
}and So this PR has nothing in common with DOM manipulation. Chances are it's probably just a big misunderstanding. |
Thanks for watching Flutterando's video. I make myself available for any possible repair, if needed, using another video explaining better what happened. That said, I still don't agree to change something from the core to serve a package. I'm also against replacing Provider with Riverpod. |
|
Thanks, I appreciate the offer for a correction. I don't like that some people decided to review-brigade the PR. But I don't think you're responsible for that. So all is well 😄 |
This both:
fixes #106549, fixes #106546
This PR should not be merged immediately. This is an attempt to see if such change is something the Flutter team is willing to accept.
If it is accepted, I'd like to have a bit of time to integrate it in Riverpod, to make sure that this fully matches my needs.
Quick tests seems to indicate that yes, but I'd have to migrate hundreds of tests to be certain.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.