Unverified Commit 1dd24b8a authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] improve Notification API performance by skipping full Element tree traversal (#98451)

parent 38dbbb17
...@@ -13,6 +13,7 @@ import 'binding.dart'; ...@@ -13,6 +13,7 @@ import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'inherited_model.dart'; import 'inherited_model.dart';
import 'notification_listener.dart';
export 'package:flutter/foundation.dart' show export 'package:flutter/foundation.dart' show
factory, factory,
...@@ -2390,6 +2391,13 @@ abstract class BuildContext { ...@@ -2390,6 +2391,13 @@ abstract class BuildContext {
/// data down to them. /// data down to them.
void visitChildElements(ElementVisitor visitor); void visitChildElements(ElementVisitor visitor);
/// Start bubbling this notification at the given build context.
///
/// The notification will be delivered to any [NotificationListener] widgets
/// with the appropriate type parameters that are ancestors of the given
/// [BuildContext].
void dispatchNotification(Notification notification);
/// Returns a description of the [Element] associated with the current build context. /// Returns a description of the [Element] associated with the current build context.
/// ///
/// The `name` is typically something like "The element being rebuilt was". /// The `name` is typically something like "The element being rebuilt was".
...@@ -3101,6 +3109,39 @@ class BuildOwner { ...@@ -3101,6 +3109,39 @@ class BuildOwner {
} }
} }
/// Mixin this class to allow receiving [Notification] objects dispatched by
/// child elements.
///
/// See also:
/// * [NotificationListener], for a widget that allows consuming notifications.
mixin NotifiableElementMixin on Element {
/// Called when a notification of the appropriate type arrives at this
/// location in the tree.
///
/// Return true to cancel the notification bubbling. Return false to
/// allow the notification to continue to be dispatched to further ancestors.
bool onNotification(Notification notification);
@override
void attachNotificationTree() {
_notificationTree = _NotificationNode(_parent?._notificationTree, this);
}
}
class _NotificationNode {
_NotificationNode(this.parent, this.current);
NotifiableElementMixin? current;
_NotificationNode? parent;
void dispatchNotification(Notification notification) {
if (current?.onNotification(notification) ?? true) {
return;
}
parent?.dispatchNotification(notification);
}
}
/// An instantiation of a [Widget] at a particular location in the tree. /// An instantiation of a [Widget] at a particular location in the tree.
/// ///
/// Widgets describe how to configure a subtree but the same widget can be used /// Widgets describe how to configure a subtree but the same widget can be used
...@@ -3161,6 +3202,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3161,6 +3202,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
Element? _parent; Element? _parent;
DebugReassembleConfig? _debugReassembleConfig; DebugReassembleConfig? _debugReassembleConfig;
_NotificationNode? _notificationTree;
/// Compare two widgets for equality. /// Compare two widgets for equality.
/// ///
...@@ -3614,6 +3656,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3614,6 +3656,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
owner!._registerGlobalKey(key, this); owner!._registerGlobalKey(key, this);
} }
_updateInheritance(); _updateInheritance();
attachNotificationTree();
} }
void _debugRemoveGlobalKeyReservation(Element child) { void _debugRemoveGlobalKeyReservation(Element child) {
...@@ -3950,6 +3993,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -3950,6 +3993,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_dependencies?.clear(); _dependencies?.clear();
_hadUnsatisfiedDependencies = false; _hadUnsatisfiedDependencies = false;
_updateInheritance(); _updateInheritance();
attachNotificationTree();
if (_dirty) if (_dirty)
owner!.scheduleBuildFor(this); owner!.scheduleBuildFor(this);
if (hadDependencies) if (hadDependencies)
...@@ -4231,6 +4275,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4231,6 +4275,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return ancestor; return ancestor;
} }
/// Called in [Element.mount] and [Element.activate] to register this element in
/// the notification tree.
///
/// This method is only exposed so that [NotifiableElementMixin] can be implemented.
/// Subclasses of [Element] that wish to respond to notifications should mix that
/// in instead.
///
/// See also:
/// * [NotificationListener], a widget that allows listening to notifications.
@protected
void attachNotificationTree() {
_notificationTree = _parent?._notificationTree;
}
void _updateInheritance() { void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active); assert(_lifecycleState == _ElementLifecycle.active);
_inheritedLookup = _parent?._inheritedLookup; _inheritedLookup = _parent?._inheritedLookup;
...@@ -4359,6 +4417,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4359,6 +4417,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return chain; return chain;
} }
@override
void dispatchNotification(Notification notification) {
_notificationTree?.dispatchNotification(notification);
}
/// A short, textual description of this element. /// A short, textual description of this element.
@override @override
String toStringShort() => _widget?.toStringShort() ?? '${describeIdentity(this)}(DEFUNCT)'; String toStringShort() => _widget?.toStringShort() ?? '${describeIdentity(this)}(DEFUNCT)';
......
...@@ -50,27 +50,6 @@ abstract class Notification { ...@@ -50,27 +50,6 @@ abstract class Notification {
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
const Notification(); const Notification();
/// Applied to each ancestor of the [dispatch] target.
///
/// The [Notification] class implementation of this method dispatches the
/// given [Notification] to each ancestor [NotificationListener] widget.
///
/// Subclasses can override this to apply additional filtering or to update
/// the notification as it is bubbled (for example, increasing a `depth` field
/// for each ancestor of a particular type).
@protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final Widget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}
/// Start bubbling this notification at the given build context. /// Start bubbling this notification at the given build context.
/// ///
/// The notification will be delivered to any [NotificationListener] widgets /// The notification will be delivered to any [NotificationListener] widgets
...@@ -78,9 +57,7 @@ abstract class Notification { ...@@ -78,9 +57,7 @@ abstract class Notification {
/// [BuildContext]. If the [BuildContext] is null, the notification is not /// [BuildContext]. If the [BuildContext] is null, the notification is not
/// dispatched. /// dispatched.
void dispatch(BuildContext? target) { void dispatch(BuildContext? target) {
// The `target` may be null if the subtree the notification is supposed to be target?.dispatchNotification(this);
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
} }
@override @override
...@@ -112,20 +89,13 @@ abstract class Notification { ...@@ -112,20 +89,13 @@ abstract class Notification {
/// [runtimeType] is a subtype of `T`. /// [runtimeType] is a subtype of `T`.
/// ///
/// To dispatch notifications, use the [Notification.dispatch] method. /// To dispatch notifications, use the [Notification.dispatch] method.
class NotificationListener<T extends Notification> extends StatelessWidget { class NotificationListener<T extends Notification> extends ProxyWidget {
/// Creates a widget that listens for notifications. /// Creates a widget that listens for notifications.
const NotificationListener({ const NotificationListener({
Key? key, Key? key,
required this.child, required Widget child,
this.onNotification, this.onNotification,
}) : super(key: key); }) : super(key: key, child: child);
/// The widget directly below this widget in the tree.
///
/// This is not necessarily the widget that dispatched the notification.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// Called when a notification of the appropriate type arrives at this /// Called when a notification of the appropriate type arrives at this
/// location in the tree. /// location in the tree.
...@@ -133,9 +103,6 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -133,9 +103,6 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// Return true to cancel the notification bubbling. Return false to /// Return true to cancel the notification bubbling. Return false to
/// allow the notification to continue to be dispatched to further ancestors. /// allow the notification to continue to be dispatched to further ancestors.
/// ///
/// The notification's [Notification.visitAncestor] method is called for each
/// ancestor, and invokes this callback as appropriate.
///
/// Notifications vary in terms of when they are dispatched. There are two /// Notifications vary in terms of when they are dispatched. There are two
/// main possibilities: dispatch between frames, and dispatch during layout. /// main possibilities: dispatch between frames, and dispatch during layout.
/// ///
...@@ -146,16 +113,29 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -146,16 +113,29 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// widgets that depend on layout, consider a [LayoutBuilder] instead. /// widgets that depend on layout, consider a [LayoutBuilder] instead.
final NotificationListenerCallback<T>? onNotification; final NotificationListenerCallback<T>? onNotification;
bool _dispatch(Notification notification, Element element) { @override
if (onNotification != null && notification is T) { Element createElement() {
final bool result = onNotification!(notification); return _NotificationElement<T>(this);
return result == true; // so that null and false have the same effect }
}
/// An element used to host [NotificationListener] elements.
class _NotificationElement<T extends Notification> extends ProxyElement with NotifiableElementMixin {
_NotificationElement(NotificationListener<T> widget) : super(widget);
@override
bool onNotification(Notification notification) {
final NotificationListener<T> listener = widget as NotificationListener<T>;
if (listener.onNotification != null && notification is T) {
return listener.onNotification!(notification);
} }
return false; return false;
} }
@override @override
Widget build(BuildContext context) => child; void notifyClients(covariant ProxyWidget oldWidget) {
// Notification tree does not need to notify clients.
}
} }
/// Indicates that the layout of one of the descendants of the object receiving /// Indicates that the layout of one of the descendants of the object receiving
...@@ -186,4 +166,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -186,4 +166,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// useful for paint effects that depend on the layout. If you were to use this /// useful for paint effects that depend on the layout. If you were to use this
/// notification to change the build, for instance, you would always be one /// notification to change the build, for instance, you would always be one
/// frame behind, which would look really ugly and laggy. /// frame behind, which would look really ugly and laggy.
class LayoutChangedNotification extends Notification { } class LayoutChangedNotification extends Notification {
/// Create a new [LayoutChangedNotification].
const LayoutChangedNotification();
}
...@@ -24,13 +24,6 @@ mixin ViewportNotificationMixin on Notification { ...@@ -24,13 +24,6 @@ mixin ViewportNotificationMixin on Notification {
int get depth => _depth; int get depth => _depth;
int _depth = 0; int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
_depth += 1;
return super.visitAncestor(element);
}
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
...@@ -38,6 +31,22 @@ mixin ViewportNotificationMixin on Notification { ...@@ -38,6 +31,22 @@ mixin ViewportNotificationMixin on Notification {
} }
} }
/// A mixin that allows [Element]s containing [Viewport] like widgets to correctly
/// modify the notification depth of a [ViewportNotificationMixin].
///
/// See also:
/// * [Viewport], which creates a custom [MultiChildRenderObjectElement] that mixes
/// this in.
mixin ViewportElementMixin on NotifiableElementMixin {
@override
bool onNotification(Notification notification) {
if (notification is ViewportNotificationMixin) {
notification._depth += 1;
}
return false;
}
}
/// A [Notification] related to scrolling. /// A [Notification] related to scrolling.
/// ///
/// [Scrollable] widgets notify their ancestors about scrolling-related changes. /// [Scrollable] widgets notify their ancestors about scrolling-related changes.
......
...@@ -318,6 +318,15 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget { ...@@ -318,6 +318,15 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget {
..offset = offset ..offset = offset
..clipBehavior = clipBehavior; ..clipBehavior = clipBehavior;
} }
@override
SingleChildRenderObjectElement createElement() {
return _SingleChildViewportElement(this);
}
}
class _SingleChildViewportElement extends SingleChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin {
_SingleChildViewportElement(_SingleChildViewport widget) : super(widget);
} }
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport { class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
......
...@@ -28,7 +28,10 @@ import 'notification_listener.dart'; ...@@ -28,7 +28,10 @@ import 'notification_listener.dart';
/// ///
/// * [SizeChangedLayoutNotifier], which sends this notification. /// * [SizeChangedLayoutNotifier], which sends this notification.
/// * [LayoutChangedNotification], of which this is a subclass. /// * [LayoutChangedNotification], of which this is a subclass.
class SizeChangedLayoutNotification extends LayoutChangedNotification { } class SizeChangedLayoutNotification extends LayoutChangedNotification {
/// Create a new [SizeChangedLayoutNotification].
const SizeChangedLayoutNotification();
}
/// A widget that automatically dispatches a [SizeChangedLayoutNotification] /// A widget that automatically dispatches a [SizeChangedLayoutNotification]
/// when the layout dimensions of its child change. /// when the layout dimensions of its child change.
...@@ -61,7 +64,7 @@ class SizeChangedLayoutNotifier extends SingleChildRenderObjectWidget { ...@@ -61,7 +64,7 @@ class SizeChangedLayoutNotifier extends SingleChildRenderObjectWidget {
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _RenderSizeChangedWithCallback( return _RenderSizeChangedWithCallback(
onLayoutChangedCallback: () { onLayoutChangedCallback: () {
SizeChangedLayoutNotification().dispatch(context); const SizeChangedLayoutNotification().dispatch(context);
}, },
); );
} }
......
...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'debug.dart'; import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'scroll_notification.dart';
export 'package:flutter/rendering.dart' show export 'package:flutter/rendering.dart' show
AxisDirection, AxisDirection,
...@@ -43,6 +44,8 @@ export 'package:flutter/rendering.dart' show ...@@ -43,6 +44,8 @@ export 'package:flutter/rendering.dart' show
/// sliver context (the opposite of this widget). /// sliver context (the opposite of this widget).
/// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its /// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its
/// contents along the main axis. /// contents along the main axis.
/// * [ViewportElementMixin], which should be mixed in to the [Element] type used
/// by viewport-like widgets to correctly handle scroll notifications.
class Viewport extends MultiChildRenderObjectWidget { class Viewport extends MultiChildRenderObjectWidget {
/// Creates a widget that is bigger on the inside. /// Creates a widget that is bigger on the inside.
/// ///
...@@ -207,7 +210,7 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -207,7 +210,7 @@ class Viewport extends MultiChildRenderObjectWidget {
} }
} }
class _ViewportElement extends MultiChildRenderObjectElement { class _ViewportElement extends MultiChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin {
/// Creates an element that uses the given widget as its configuration. /// Creates an element that uses the given widget as its configuration.
_ViewportElement(Viewport widget) : super(widget); _ViewportElement(Viewport widget) : super(widget);
......
...@@ -17,7 +17,7 @@ class NotifyMaterial extends StatelessWidget { ...@@ -17,7 +17,7 @@ class NotifyMaterial extends StatelessWidget {
const NotifyMaterial({ Key? key }) : super(key: key); const NotifyMaterial({ Key? key }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
LayoutChangedNotification().dispatch(context); const LayoutChangedNotification().dispatch(context);
return Container(); return Container();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment