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
Document why Widget.operator==
is marked as non-virtual
#49490
Comments
We should probably document this. I'm not sure why you'd want to override it though. Widgets are expected to be immutable. Even though Dart can't really guarantee immutability of e.g. a child list, the framework breaks if you mutate a property of the widget. The default implementations of equals (i.e. /cc @Hixie @goderbauer who probably have more insight into this. |
Flutter optimize rebuilds around the == operator. Overriding == allows filtering rebuilds in situations that would otherwise rebuild for no reason. It's mostly for leaf nodes though. |
Can you give an example? |
class Leaf extends StatelessWidget {
const Leaf({Key key, this.label}) : super(key: key);
final String label;
@override
bool operator ==(Object other) {
return other is Leaf && other.key == key && other.label == label;
}
@override
int get hashCode => hashValues(key, label);
@override
Widget build(BuildContext context) {
return Text(label);
}
} This makes the widget behaves like a |
Some context for why this was done is in the linked from the PR that added the |
@goderbauer - I did look through that and saw a few places where @Hixie said not to do it, but never why. |
It was likely about non-leaf widgets. If a widget has a child, and that child too overrides ==, then we could end up effectively comparing the entire widget tree. But I don't see any issue with leaf widgets though. |
Do we have any benchmarks for the leaf case? I wouldn't be surprised if the mere existence of that operator== made everything more expensive (since it turns a single-dispatch into a dynamic dispatch for every single widget comparison ever). |
I think if we don't want developers to implement |
That could be confusing - someone expects the framework to be using their overridden |
Regardless of what choice we make here (keeping nonVirtual, using identity) we won't be able to get away with documenting it extensively. equality as a concept is too heavily overloaded |
I meant "without" documenting it extensively |
I am pretty sure there are cases when |
If |
@goderbauer There are two things that can affect performance: the performance of the comparison itself, and the performance of the framework due to changes in comparison semantics. The former is probably relatively small. The latter can be much bigger depending on the app. For example, the comparison can shut off the rebuilding of an entire subtree of an app if the comparison returns |
To be clear, I'm in favor of using the |
@yjbanov |
@t-artikov With the Here's a quick example: class Cache<T> extends StatefulWidget {
const Cache({Key key, this.builder, this.value}) : super(key: key);
final Widget Function(BuildContext context, T value) builder;
final T value;
@override
_CacheState<T> createState() => _CacheState<T>();
}
class _CacheState<T> extends State<Cache<T>> {
Widget cache;
T previousValue;
@override
Widget build(BuildContext context) {
if (widget.value != previousValue) {
previousValue = widget.value;
cache = Builder(
builder: (context) => widget.builder(context, widget.value),
);
}
return cache;
}
} Then used this way: Cache<int>(
value: 42,
builder: (context, value) {
return Text('$value');
},
); Where It is less ideal for multiple reasons though. |
@rrousselGit
I agree, in my opinion, it:
|
Aren't keys helpful here? |
I was actually just in the process of running some more benchmarks. I modified the stocks app slightly (source) to make sure it can benefit from overriding It's pretty clear that overriding Overriding The cache approach seems more flutter-like ("everything is a widget") and you get the same performance. Developers are also already familiar with all concepts involved here and don't have to learn anything new. In my opinion, we should leave things as they are and repurpose this bug as an issue to document what you should do instead if you think you want to override |
But it can be done automatically. Caching, on the other hand, is very tedious to write and cannot be automatized. |
Also, by using cache instead of ==, we are reverting from declarative programming to imperative programming, and have to manage a state. The resulting code is a lot worse than what == does, in my opinion that is. |
I believe you could also write a code generator that generates this general caching pattern shown in my change: goderbauer@8ebb668 |
I agree with that. Implementing the caching pattern requires a stateful widget and some more work. Overriding == is way simpler.
It should be definitely documented that overriding == has to be done carefully to avoid comparing whole subtrees. It should only be recommended for widgets that do not take children. |
@dnfield I'm actually surprised that adding keys in a import 'package:flutter/material.dart';
void main() {
final app = MaterialApp(
home: MyHomePage(),
);
runApp(app);
}
class Item {
const Item(this.title, this.subtitle);
final String title;
final String subtitle;
static Item createByIndex(int index) {
return Item('Item $index', 'Subtitle $index');
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final items = List.generate(5, Item.createByIndex);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Rebuild Optimization'),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemView(items[index], ValueKey(index));
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
setState(() {
items.add(Item.createByIndex(items.length));
});
},
),
);
}
}
class ItemView extends StatelessWidget {
const ItemView(this.item, Key key) : super(key: key);
final Item item;
@override
Widget build(BuildContext context) {
print('build ${item.title}');
return ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
);
}
} |
* Add a test for tracing the framework. * Add a `DiagnosticsNode.toTimelineArguments()` that can be used easily with `Timeline.startSync`. * Rename some of the timeline events for clarity: hot reload dirtying now says "Hot Reload" for example, and the phases are in all caps (BUILD, LAYOUT, etc). * Always flag intrinsic and dry layout logic in debug and profile builds. Normally this only flags one event; when `debugProfileLayoutsEnabled` is true it shows the entire path. Might be good to have this flagged in the DevTools. Fixes flutter#93031. * Fix a broken link by pointing to our official documentation instead of Fuchsia documentation. Fixes flutter#92044. * Change how painting is traced when `debugProfilePaintsEnabled` is on to be more comprehensive. * Add some details to the top-level timeline events, e.g. what nodes are dirty, when the relevant `debugProfile*Enabled` flags are on. * Include the Diagnosticable information about a `RenderObject` or a `Widget` in the information logged when `debugProfileLayoutsEnabled`, `debugProfilePaintsEnabled`, or `debugProfileBuildsEnabled` are on. Fixes flutter#93009 * Refactor how tracing is done in the widgets library so that `RenderObjectWidget`s are included in the timeline when `debugProfileBuildsEnabled` is on. Fixes flutter#93007. Also: * Fix a minor error we introduced with the null safety migration: `DiagnosticsNode.toDescription` should return non-null. (Most of the logic either assumed that or already enforced it.) * Implement `debugFillProperties` for `Placeholder` and `RenderCustomPaint`, to help with the tests that were added. * Remove a TODO in `objects.dart` that we're never going to get to. * Improve some docs on `BuildContext`. * Cache `_dirtyElements[index]` in `buildScope`. * Improve docs for `operator ==` on `Widget`. Fixes flutter#49490. * Remove some code duplication in `framework.dart`. * Clean up `_NullWidget` a bit.
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of |
Why is Widget.operator== marked as non-virtual?
Trying to override == on a widget triggers a warning, but there's no explanation on why (neither in the warning nor in the sources), and is not immediately obvious as to why we shouldn't do so.
The text was updated successfully, but these errors were encountered: