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';
import 'debug.dart';
import 'focus_manager.dart';
import 'inherited_model.dart';
import 'notification_listener.dart';
export 'package:flutter/foundation.dart' show
factory,
......@@ -2390,6 +2391,13 @@ abstract class BuildContext {
/// data down to them.
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.
///
/// The `name` is typically something like "The element being rebuilt was".
......@@ -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.
///
/// 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 {
Element? _parent;
DebugReassembleConfig? _debugReassembleConfig;
_NotificationNode? _notificationTree;
/// Compare two widgets for equality.
///
......@@ -3614,6 +3656,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
owner!._registerGlobalKey(key, this);
}
_updateInheritance();
attachNotificationTree();
}
void _debugRemoveGlobalKeyReservation(Element child) {
......@@ -3950,6 +3993,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();
attachNotificationTree();
if (_dirty)
owner!.scheduleBuildFor(this);
if (hadDependencies)
......@@ -4231,6 +4275,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
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() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedLookup = _parent?._inheritedLookup;
......@@ -4359,6 +4417,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
return chain;
}
@override
void dispatchNotification(Notification notification) {
_notificationTree?.dispatchNotification(notification);
}
/// A short, textual description of this element.
@override
String toStringShort() => _widget?.toStringShort() ?? '${describeIdentity(this)}(DEFUNCT)';
......
......@@ -50,27 +50,6 @@ abstract class Notification {
/// const constructors so that they can be used in const expressions.
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.
///
/// The notification will be delivered to any [NotificationListener] widgets
......@@ -78,9 +57,7 @@ abstract class Notification {
/// [BuildContext]. If the [BuildContext] is null, the notification is not
/// dispatched.
void dispatch(BuildContext? target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
target?.dispatchNotification(this);
}
@override
......@@ -112,20 +89,13 @@ abstract class Notification {
/// [runtimeType] is a subtype of `T`.
///
/// 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.
const NotificationListener({
Key? key,
required this.child,
required Widget child,
this.onNotification,
}) : super(key: key);
/// 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;
}) : super(key: key, child: child);
/// Called when a notification of the appropriate type arrives at this
/// location in the tree.
......@@ -133,9 +103,6 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// Return true to cancel the notification bubbling. Return false to
/// 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
/// main possibilities: dispatch between frames, and dispatch during layout.
///
......@@ -146,16 +113,29 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
/// widgets that depend on layout, consider a [LayoutBuilder] instead.
final NotificationListenerCallback<T>? onNotification;
bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
final bool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
@override
Element createElement() {
return _NotificationElement<T>(this);
}
}
/// 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;
}
@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
......@@ -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
/// notification to change the build, for instance, you would always be one
/// 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 {
int get depth => _depth;
int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
_depth += 1;
return super.visitAncestor(element);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
......@@ -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.
///
/// [Scrollable] widgets notify their ancestors about scrolling-related changes.
......
......@@ -318,6 +318,15 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget {
..offset = offset
..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 {
......
......@@ -28,7 +28,10 @@ import 'notification_listener.dart';
///
/// * [SizeChangedLayoutNotifier], which sends this notification.
/// * [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]
/// when the layout dimensions of its child change.
......@@ -61,7 +64,7 @@ class SizeChangedLayoutNotifier extends SingleChildRenderObjectWidget {
RenderObject createRenderObject(BuildContext context) {
return _RenderSizeChangedWithCallback(
onLayoutChangedCallback: () {
SizeChangedLayoutNotification().dispatch(context);
const SizeChangedLayoutNotification().dispatch(context);
},
);
}
......
......@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'debug.dart';
import 'framework.dart';
import 'scroll_notification.dart';
export 'package:flutter/rendering.dart' show
AxisDirection,
......@@ -43,6 +44,8 @@ export 'package:flutter/rendering.dart' show
/// sliver context (the opposite of this widget).
/// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its
/// 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 {
/// Creates a widget that is bigger on the inside.
///
......@@ -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.
_ViewportElement(Viewport widget) : super(widget);
......
......@@ -17,7 +17,7 @@ class NotifyMaterial extends StatelessWidget {
const NotifyMaterial({ Key? key }) : super(key: key);
@override
Widget build(BuildContext context) {
LayoutChangedNotification().dispatch(context);
const LayoutChangedNotification().dispatch(context);
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