diff --git a/packages/flutter/lib/src/foundation/change_notifier.dart b/packages/flutter/lib/src/foundation/change_notifier.dart index 0b8c6f4ae9..d8ab7d5720 100644 --- a/packages/flutter/lib/src/foundation/change_notifier.dart +++ b/packages/flutter/lib/src/foundation/change_notifier.dart @@ -207,6 +207,39 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; + /// Dispatches event of object creation to [MemoryAllocations.instance]. + /// + /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] + /// is false, the method is noop. + /// + /// Tools like leak_tracker use the event of object creation to help + /// developers identify the owner of the object, for troubleshooting purposes, + /// by taking stack trace at the moment of the event. + /// + /// But, as [ChangeNotifier] is mixin, it does not have its own constructor. So, it + /// communicates object creation in first `addListener`, that results + /// in the stack trace pointing to `addListener`, not to constructor. + /// + /// To make debugging easier, invoke [ChangeNotifier.maybeDispatchObjectCreation] + /// in constructor of the class. It will help + /// to identify the owner. + /// + /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` + /// so that the method is tree-shaken away when the flag is false. + @protected + void maybeDispatchObjectCreation() { + // Tree shaker does not include this method and the class MemoryAllocations + // if kFlutterMemoryAllocationsEnabled is false. + if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { + MemoryAllocations.instance.dispatchObjectCreated( + library: _flutterFoundationLibrary, + className: '$ChangeNotifier', + object: this, + ); + _creationDispatched = true; + } + } + /// Register a closure to be called when the object changes. /// /// If the given closure is already registered, an additional instance is @@ -236,14 +269,11 @@ mixin class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ChangeNotifier', - object: this, - ); - _creationDispatched = true; + + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(); } + if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -505,13 +535,8 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ValueNotifier', - object: this, - ); + maybeDispatchObjectCreation(); } - _creationDispatched = true; } /// The current value stored in this notifier. diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart index 5bc3879de8..5c66c42997 100644 --- a/packages/flutter/lib/src/widgets/shortcuts.dart +++ b/packages/flutter/lib/src/widgets/shortcuts.dart @@ -1196,6 +1196,13 @@ class ShortcutRegistryEntry { /// widgets that are not descendants of the registry can listen to it (e.g. in /// overlays). class ShortcutRegistry with ChangeNotifier { + /// Creates an instance of [ShortcutRegistry]. + ShortcutRegistry() { + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(); + } + } + bool _notificationScheduled = false; bool _disposed = false; diff --git a/packages/flutter/lib/src/widgets/snapshot_widget.dart b/packages/flutter/lib/src/widgets/snapshot_widget.dart index 854512f1f4..7d509ce28b 100644 --- a/packages/flutter/lib/src/widgets/snapshot_widget.dart +++ b/packages/flutter/lib/src/widgets/snapshot_widget.dart @@ -482,4 +482,8 @@ class _DefaultSnapshotPainter implements SnapshotPainter { @override bool shouldRepaint(covariant _DefaultSnapshotPainter oldPainter) => false; + + @override + @protected + void maybeDispatchObjectCreation() { } } diff --git a/packages/flutter/test/widgets/shortcuts_test.dart b/packages/flutter/test/widgets/shortcuts_test.dart index 0481f8cd6b..064fdfb231 100644 --- a/packages/flutter/test/widgets/shortcuts_test.dart +++ b/packages/flutter/test/widgets/shortcuts_test.dart @@ -1852,6 +1852,22 @@ void main() { }, throwsAssertionError); token.dispose(); }); + + testWidgets('dispatches object creation in constructor', (WidgetTester tester) async { + final MemoryAllocations ma = MemoryAllocations.instance; + assert(!ma.hasListeners); + int eventCount = 0; + void listener(ObjectEvent event) => eventCount++; + ma.addListener(listener); + + final ShortcutRegistry registry = ShortcutRegistry(); + + expect(eventCount, 1); + + registry.dispose(); + ma.removeListener(listener); + assert(!ma.hasListeners); + }); }); }