From 4c07a9da43cb4b1d5b2f83fd9a556ea308e30ec6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 20 Oct 2023 00:59:37 +0300 Subject: [PATCH] memory leak tracking & fixes --- lib/main_common.dart | 6 +++++ lib/model/app/dependencies.dart | 5 ++++ lib/model/entry/entry.dart | 13 ++++++++-- lib/model/query.dart | 6 +++++ lib/model/source/collection_lens.dart | 2 ++ lib/services/analysis_service.dart | 11 ++++++++ lib/widgets/about/licenses.dart | 6 +++++ .../collection/entry_set_action_delegate.dart | 1 - .../collection/grid/list_details_theme.dart | 13 +++++----- .../quick_choosers/common/button.dart | 1 + .../common/basic/text/animated_diff.dart | 7 +++--- .../common/basic/text/background_painter.dart | 1 + .../common/behaviour/pop/double_back.dart | 14 +++++++++++ lib/widgets/common/grid/header.dart | 6 +++-- .../identity/buttons/captioned_button.dart | 5 ++-- lib/widgets/common/map/geo_map.dart | 1 + lib/widgets/common/search/delegate.dart | 14 ++++++++++- lib/widgets/common/thumbnail/image.dart | 1 + .../common/tile_extent_controller.dart | 11 ++++++++ lib/widgets/debug/app_debug_page.dart | 19 ++++++++++++++ .../cover_selection_dialog.dart | 5 ++-- lib/widgets/editor/transform/controller.dart | 10 ++++++++ .../common/action_delegates/chip_set.dart | 1 - .../common/list_details_theme.dart | 14 ++++++----- lib/widgets/map/map_page.dart | 10 +++++--- .../common/quick_actions/editor_page.dart | 1 + .../action/entry_info_action_delegate.dart | 1 - .../viewer/action/video_action_delegate.dart | 14 ++++++++++- lib/widgets/viewer/controls/controller.dart | 11 ++++++++ lib/widgets/viewer/entry_viewer_page.dart | 3 +++ lib/widgets/viewer/entry_viewer_stack.dart | 3 +++ lib/widgets/viewer/info/common.dart | 6 +++-- lib/widgets/viewer/info/location_section.dart | 1 - lib/widgets/viewer/multipage/controller.dart | 10 ++++++++ lib/widgets/viewer/video/conductor.dart | 1 + lib/widgets/viewer/view/controller.dart | 14 ++++++++++- .../visual/video/subtitle/subtitle.dart | 7 +++--- .../lib/src/controller/controller.dart | 25 +++++++++++++------ plugins/aves_map/lib/src/controller.dart | 14 +++++++++++ plugins/aves_video/lib/src/controller.dart | 10 ++++++++ .../aves_video_ijk/lib/src/controller.dart | 1 + .../aves_video_mpv/lib/src/controller.dart | 1 + pubspec.lock | 8 ++++++ pubspec.yaml | 1 + 44 files changed, 270 insertions(+), 45 deletions(-) diff --git a/lib/main_common.dart b/lib/main_common.dart index f525bab0d..91736a6e7 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -3,7 +3,9 @@ import 'dart:isolate'; import 'package:aves/app_flavor.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:leak_tracker/leak_tracker.dart'; void mainCommon(AppFlavor flavor, {Map? debugIntentData}) { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -35,5 +37,9 @@ void mainCommon(AppFlavor flavor, {Map? debugIntentData}) { // ErrorWidget.builder = (details) => ErrorWidget(details.exception); // cf https://docs.flutter.dev/testing/errors + LeakTracking.start(); + MemoryAllocations.instance.addListener( + (event) => LeakTracking.dispatchObjectEvent(event.toMap()), + ); runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData)); } diff --git a/lib/model/app/dependencies.dart b/lib/model/app/dependencies.dart index 90ec993aa..38da17b19 100644 --- a/lib/model/app/dependencies.dart +++ b/lib/model/app/dependencies.dart @@ -347,6 +347,11 @@ class Dependencies { licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE', sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', ), + Dependency( + name: 'Memory Leak Tracker', + license: bsd3, + sourceUrl: 'https://github.com/dart-lang/leak_tracker', + ), Dependency( name: 'Path', license: bsd3, diff --git a/lib/model/entry/entry.dart b/lib/model/entry/entry.dart index a58d369b2..5b8f7c12d 100644 --- a/lib/model/entry/entry.dart +++ b/lib/model/entry/entry.dart @@ -49,8 +49,7 @@ class AvesEntry with AvesEntryBase { @override final AChangeNotifier visualChangeNotifier = AChangeNotifier(); - final AChangeNotifier metadataChangeNotifier = AChangeNotifier(); - final AChangeNotifier addressChangeNotifier = AChangeNotifier(); + final AChangeNotifier metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); AvesEntry({ required int? id, @@ -72,6 +71,13 @@ class AvesEntry with AvesEntryBase { required this.origin, this.burstEntries, }) : id = id ?? 0 { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AvesEntry', + object: this, + ); + } this.path = path; this.sourceTitle = sourceTitle; this.dateModifiedSecs = dateModifiedSecs; @@ -181,6 +187,9 @@ class AvesEntry with AvesEntryBase { } void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } visualChangeNotifier.dispose(); metadataChangeNotifier.dispose(); addressChangeNotifier.dispose(); diff --git a/lib/model/query.dart b/lib/model/query.dart index 29d950240..79d63e47a 100644 --- a/lib/model/query.dart +++ b/lib/model/query.dart @@ -16,6 +16,12 @@ class Query extends ChangeNotifier { } } + @override + void dispose() { + _focusRequestNotifier.dispose(); + super.dispose(); + } + bool _enabled = false; bool get enabled => _enabled; diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 23af9b736..b765c9f58 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -99,6 +99,8 @@ class CollectionLens with ChangeNotifier { ..forEach((sub) => sub.cancel()) ..clear(); favourites.removeListener(_onFavouritesChanged); + filterChangeNotifier.dispose(); + sortSectionChangeNotifier.dispose(); super.dispose(); } diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index eeb91fb8b..031dc54b6 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -10,6 +10,7 @@ import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/view/view.dart'; import 'package:aves_model/aves_model.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -92,12 +93,22 @@ class Analyzer { Analyzer() { debugPrint('$runtimeType create'); + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$Analyzer', + object: this, + ); + } _serviceStateNotifier.addListener(_onServiceStateChanged); _source.stateNotifier.addListener(_onSourceStateChanged); } void dispose() { debugPrint('$runtimeType dispose'); + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _serviceStateNotifier.removeListener(_onServiceStateChanged); _source.stateNotifier.removeListener(_onSourceStateChanged); _stopUpdateTimer(); diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 83982ae7c..76821da50 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -32,6 +32,12 @@ class _LicensesState extends State { _sortPackages(); } + @override + void dispose() { + _expandedNotifier.dispose(); + super.dispose(); + } + void _sortPackages() { int compare(Dependency a, Dependency b) => compareAsciiUpperCase(a.name, b.name); _platform.sort(compare); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 564724969..f5e764670 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -644,7 +644,6 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware builder: (context) => MapPage(collection: mapCollection), ), ); - mapCollection.dispose(); } void _goToSlideshow(BuildContext context) { diff --git a/lib/widgets/collection/grid/list_details_theme.dart b/lib/widgets/collection/grid/list_details_theme.dart index e9f666570..6dac8807d 100644 --- a/lib/widgets/collection/grid/list_details_theme.dart +++ b/lib/widgets/collection/grid/list_details_theme.dart @@ -32,20 +32,21 @@ class EntryListDetailsTheme extends StatelessWidget { final titleStyle = textTheme.bodyMedium!; final captionStyle = textTheme.bodySmall!; - final titleLineHeight = (RenderParagraph( + final titleLineHeightParagraph = RenderParagraph( TextSpan(text: 'Fake Title', style: titleStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, - )..layout(const BoxConstraints(), parentUsesSize: true)) - .getMaxIntrinsicHeight(double.infinity); + )..layout(const BoxConstraints(), parentUsesSize: true); + final titleLineHeight = titleLineHeightParagraph.getMaxIntrinsicHeight(double.infinity); + titleLineHeightParagraph.dispose(); - final captionLineHeight = (RenderParagraph( + final captionLineHeightParagraph = RenderParagraph( TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, strutStyle: AStyles.overflowStrut, - )..layout(const BoxConstraints(), parentUsesSize: true)) - .getMaxIntrinsicHeight(double.infinity); + )..layout(const BoxConstraints(), parentUsesSize: true); + final captionLineHeight = captionLineHeightParagraph.getMaxIntrinsicHeight(double.infinity); var titleMaxLines = 1; var showDate = false; diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index eddc578a2..bfcaa6b4d 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -49,6 +49,7 @@ abstract class ChooserQuickButtonState, U> exten void dispose() { _animationController?.dispose(); _clearChooserOverlayEntry(); + _chooserValueNotifier.dispose(); super.dispose(); } diff --git a/lib/widgets/common/basic/text/animated_diff.dart b/lib/widgets/common/basic/text/animated_diff.dart index 091804f12..06c642224 100644 --- a/lib/widgets/common/basic/text/animated_diff.dart +++ b/lib/widgets/common/basic/text/animated_diff.dart @@ -114,14 +114,15 @@ class _AnimatedDiffTextState extends State with SingleTickerPr } Size textSize(String text) { - final para = RenderParagraph( + final paragraph = RenderParagraph( TextSpan(text: text, style: widget.textStyle), textDirection: Directionality.of(context), textScaleFactor: MediaQuery.textScaleFactorOf(context), strutStyle: widget.strutStyle, )..layout(const BoxConstraints(), parentUsesSize: true); - final width = para.getMaxIntrinsicWidth(double.infinity); - final height = para.getMaxIntrinsicHeight(double.infinity); + final width = paragraph.getMaxIntrinsicWidth(double.infinity); + final height = paragraph.getMaxIntrinsicHeight(double.infinity); + paragraph.dispose(); return Size(width, height); } diff --git a/lib/widgets/common/basic/text/background_painter.dart b/lib/widgets/common/basic/text/background_painter.dart index e738614fc..465837176 100644 --- a/lib/widgets/common/basic/text/background_painter.dart +++ b/lib/widgets/common/basic/text/background_painter.dart @@ -44,6 +44,7 @@ class TextBackgroundPainter extends StatelessWidget { TextSelection(baseOffset: 0, extentOffset: textLength), boxHeightStyle: ui.BoxHeightStyle.max, ); + paragraph.dispose(); // merge boxes to avoid artifacts at box edges, from anti-aliasing and rounding hacks final lineRects = groupBy(allBoxes, (v) => v.top).entries.map((kv) { diff --git a/lib/widgets/common/behaviour/pop/double_back.dart b/lib/widgets/common/behaviour/pop/double_back.dart index e2dd820e9..948946c8a 100644 --- a/lib/widgets/common/behaviour/pop/double_back.dart +++ b/lib/widgets/common/behaviour/pop/double_back.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; @@ -10,7 +11,20 @@ class DoubleBackPopHandler { bool _backOnce = false; Timer? _backTimer; + DoubleBackPopHandler() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$DoubleBackPopHandler', + object: this, + ); + } + } + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _stopBackTimer(); } diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index ce4213cc2..4769eae1e 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -127,7 +127,7 @@ class SectionHeader extends StatelessWidget { }) { final textScaleFactor = MediaQuery.textScaleFactorOf(context); final maxContentWidth = maxWidth - (SectionHeader.padding.horizontal + SectionHeader.margin.horizontal); - final para = RenderParagraph( + final paragraph = RenderParagraph( TextSpan( children: [ // as of Flutter v3.7.7, `RenderParagraph` fails to lay out `WidgetSpan` offscreen @@ -148,7 +148,9 @@ class SectionHeader extends StatelessWidget { textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, )..layout(BoxConstraints(maxWidth: maxContentWidth), parentUsesSize: true); - return para.getMaxIntrinsicHeight(maxContentWidth); + final height = paragraph.getMaxIntrinsicHeight(maxContentWidth); + paragraph.dispose(); + return height; } } diff --git a/lib/widgets/common/identity/buttons/captioned_button.dart b/lib/widgets/common/identity/buttons/captioned_button.dart index 10cc09602..d697f1eb1 100644 --- a/lib/widgets/common/identity/buttons/captioned_button.dart +++ b/lib/widgets/common/identity/buttons/captioned_button.dart @@ -38,13 +38,14 @@ class CaptionedButton extends StatefulWidget { final width = getWidth(context); var height = width; if (showCaption) { - final para = RenderParagraph( + final paragraph = RenderParagraph( TextSpan(text: text, style: CaptionedButtonText.textStyle(context)), textDirection: TextDirection.ltr, textScaleFactor: MediaQuery.textScaleFactorOf(context), maxLines: CaptionedButtonText.maxLines, )..layout(const BoxConstraints(), parentUsesSize: true); - height += para.getMaxIntrinsicHeight(width) + padding.vertical; + height += paragraph.getMaxIntrinsicHeight(width) + padding.vertical; + paragraph.dispose(); } return Size(width, height); } diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 6ab91cd2d..2f4cea000 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -113,6 +113,7 @@ class _GeoMapState extends State { @override void dispose() { + _clusterChangeNotifier.dispose(); _unregisterWidget(widget); super.dispose(); } diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 9d89bac86..b803404f8 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -2,6 +2,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/route.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -15,11 +16,22 @@ abstract class AvesSearchDelegate extends SearchDelegate { String? initialQuery, required super.searchFieldLabel, }) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AvesSearchDelegate', + object: this, + ); + } query = initialQuery ?? ''; } @mustCallSuper - void dispose() {} + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + } @override Widget? buildLeading(BuildContext context) { diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index d2e06f994..be5b43eb9 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -88,6 +88,7 @@ class _ThumbnailImageState extends State { } void _registerWidget(ThumbnailImage widget) { + // TODO TLAD [leak] `widget.entry.visualChangeNotifier` widget.entry.visualChangeNotifier.addListener(_onVisualChanged); _initProvider(); } diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart index baaaefc84..f7a953ee3 100644 --- a/lib/widgets/common/tile_extent_controller.dart +++ b/lib/widgets/common/tile_extent_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -26,6 +27,13 @@ class TileExtentController { required this.spacing, required this.horizontalPadding, }) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$TileExtentController', + object: this, + ); + } // initialize extent to 0, so that it will be dynamically sized on first launch extentNotifier = ValueNotifier(0); userPreferredExtent = settings.getTileExtent(settingsRouteKey); @@ -33,6 +41,9 @@ class TileExtentController { } void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index ffc38373d..34a38a553 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -27,9 +27,11 @@ import 'package:aves/widgets/debug/report.dart'; import 'package:aves/widgets/debug/settings.dart'; import 'package:aves/widgets/debug/storage.dart'; import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; +import 'package:leak_tracker/leak_tracker.dart'; class AppDebugPage extends StatefulWidget { static const routeName = '/debug'; @@ -133,6 +135,23 @@ class _AppDebugPageState extends State { }, title: const Text('Show tasks overlay'), ), + ElevatedButton( + onPressed: () => LeakTracking.collectLeaks().then((leaks) { + leaks.byType.forEach((type, reports) { + debugPrint('* leak type=$type'); + groupBy(reports, (report) => report.type).forEach((reportType, typedReports) { + debugPrint(' * report type=$reportType'); + groupBy(typedReports, (report) => report.trackedClass).forEach((trackedClass, classedReports) { + debugPrint(' trackedClass=$trackedClass reports=${classedReports.length}'); + // classedReports.forEach((report) { + // debugPrint(' phase=${report.phase} retainingPath=${report.retainingPath} detailedPath=${report.detailedPath} context=${report.context}'); + // }); + }); + }); + }); + }), + child: const Text('Collect leaks'), + ), ElevatedButton( onPressed: () => source.init(loadTopEntriesFirst: false), child: const Text('Source refresh (top off)'), diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index 57d093628..4ec1139ad 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -437,12 +437,13 @@ class _CoverSelectionDialogState extends State { l10n.setCoverDialogCustom, }.fold('', (previousValue, element) => '$previousValue\n$element'); - final para = RenderParagraph( + final paragraph = RenderParagraph( TextSpan(text: _optionLines, style: Theme.of(context).textTheme.titleMedium!), textDirection: TextDirection.ltr, textScaleFactor: MediaQuery.textScaleFactorOf(context), )..layout(const BoxConstraints(), parentUsesSize: true); - final textWidth = para.getMaxIntrinsicWidth(double.infinity); + final textWidth = paragraph.getMaxIntrinsicWidth(double.infinity); + paragraph.dispose(); // from `RadioListTile` layout const contentPadding = 32; diff --git a/lib/widgets/editor/transform/controller.dart b/lib/widgets/editor/transform/controller.dart index 0c414a326..b13ae269d 100644 --- a/lib/widgets/editor/transform/controller.dart +++ b/lib/widgets/editor/transform/controller.dart @@ -33,11 +33,21 @@ class TransformController { final Size displaySize; TransformController(this.displaySize) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$TransformController', + object: this, + ); + } reset(); aspectRatioNotifier.addListener(_onAspectRatioChanged); } void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } aspectRatioNotifier.dispose(); } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index daf10d73d..2f23da3ea 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -255,7 +255,6 @@ abstract class ChipSetActionDelegate with FeedbackMi builder: (context) => MapPage(collection: mapCollection), ), ); - mapCollection.dispose(); } void _goToSlideshow(BuildContext context, Set filters) { diff --git a/lib/widgets/filter_grids/common/list_details_theme.dart b/lib/widgets/filter_grids/common/list_details_theme.dart index 4176ff615..80de6ff7b 100644 --- a/lib/widgets/filter_grids/common/list_details_theme.dart +++ b/lib/widgets/filter_grids/common/list_details_theme.dart @@ -38,20 +38,22 @@ class FilterListDetailsTheme extends StatelessWidget { final captionStyle = textTheme.bodySmall!; final titleIconSize = AvesFilterChip.iconSize * textScaleFactor; - final titleLineHeight = (RenderParagraph( + final titleLineHeightParagraph = RenderParagraph( TextSpan(text: 'Fake Title', style: titleStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, - )..layout(const BoxConstraints(), parentUsesSize: true)) - .getMaxIntrinsicHeight(double.infinity); + )..layout(const BoxConstraints(), parentUsesSize: true); + final titleLineHeight = titleLineHeightParagraph.getMaxIntrinsicHeight(double.infinity); + titleLineHeightParagraph.dispose(); - final captionLineHeight = (RenderParagraph( + final captionLineHeightParagraph = RenderParagraph( TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, strutStyle: AStyles.overflowStrut, - )..layout(const BoxConstraints(), parentUsesSize: true)) - .getMaxIntrinsicHeight(double.infinity); + )..layout(const BoxConstraints(), parentUsesSize: true); + final captionLineHeight = captionLineHeightParagraph.getMaxIntrinsicHeight(double.infinity); + captionLineHeightParagraph.dispose(); var titleMaxLines = 1; var showCount = false; diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 59bf3b2f2..4ec2e759f 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -165,6 +165,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin _mapController.dispose(); _selectedIndexNotifier.removeListener(_onThumbnailIndexChanged); regionCollection?.dispose(); + // provided collection should be a new instance specifically created + // for the `MapPage` widget, so it can be safely disposed here + widget.collection.dispose(); super.dispose(); } @@ -394,10 +397,11 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), pageBuilder: (context, a, sa) { + final viewerCollection = regionCollection?.copyWith( + listenToSource: false, + ); return EntryViewerPage( - collection: regionCollection?.copyWith( - listenToSource: false, - ), + collection: viewerCollection, initialEntry: initialEntry, ); }, diff --git a/lib/widgets/settings/common/quick_actions/editor_page.dart b/lib/widgets/settings/common/quick_actions/editor_page.dart index 331dcd49d..8b8a8758a 100644 --- a/lib/widgets/settings/common/quick_actions/editor_page.dart +++ b/lib/widgets/settings/common/quick_actions/editor_page.dart @@ -104,6 +104,7 @@ class _QuickActionEditorBodyState extends State { @override void dispose() { _viewerController.dispose(); + // provided collection should be a new instance specifically created + // for the `EntryViewerPage` widget, so it can be safely disposed here + widget.collection?.dispose(); super.dispose(); } diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 01399c26f..064f1ffa1 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -192,7 +192,10 @@ class _EntryViewerStackState extends State with EntryViewContr _overlayVisible.dispose(); _viewLocked.dispose(); _overlayExpandedNotifier.dispose(); + _currentVerticalPage.dispose(); + _horizontalPager.dispose(); _verticalPager.dispose(); + _verticalScrollNotifier.dispose(); _heroInfoNotifier.dispose(); _stopOverlayHidingTimer(); AvesApp.lifecycleStateNotifier.removeListener(_onAppLifecycleStateChanged); diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 6d4981c57..710ef8c24 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -139,12 +139,14 @@ class _InfoRowGroupState extends State { } double _getSpanWidth(TextSpan span, double textScaleFactor) { - final para = RenderParagraph( + final paragraph = RenderParagraph( span, textDirection: TextDirection.ltr, textScaleFactor: textScaleFactor, )..layout(const BoxConstraints(), parentUsesSize: true); - return para.getMaxIntrinsicWidth(double.infinity); + final width = paragraph.getMaxIntrinsicWidth(double.infinity); + paragraph.dispose(); + return width; } List _buildTextValueSpans(BuildContext context, String key, String value) { diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 1010fc999..df001809f 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -153,7 +153,6 @@ class _LocationSectionState extends State { ), ), ); - mapCollection.dispose(); } void _onMetadataChanged() { diff --git a/lib/widgets/viewer/multipage/controller.dart b/lib/widgets/viewer/multipage/controller.dart index a83a8b7c3..429fb582d 100644 --- a/lib/widgets/viewer/multipage/controller.dart +++ b/lib/widgets/viewer/multipage/controller.dart @@ -24,6 +24,13 @@ class MultiPageController { set page(int? page) => pageNotifier.value = page; MultiPageController(this.entry) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$MultiPageController', + object: this, + ); + } reset(); } @@ -40,6 +47,9 @@ class MultiPageController { }); void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _disposed = true; pageNotifier.dispose(); } diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 8557b20c2..173717dac 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -25,6 +25,7 @@ class VideoConductor { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); + _controllers.forEach((v) => v.dispose()); _controllers.clear(); if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { await windowService.keepScreenOn(false); diff --git a/lib/widgets/viewer/view/controller.dart b/lib/widgets/viewer/view/controller.dart index 481163a6c..fcbcb6a5d 100644 --- a/lib/widgets/viewer/view/controller.dart +++ b/lib/widgets/viewer/view/controller.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/view_state.dart'; import 'package:aves/widgets/viewer/view/histogram.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class ViewStateController with HistogramMixin { @@ -13,9 +14,20 @@ class ViewStateController with HistogramMixin { ViewStateController({ required this.entry, required this.viewStateNotifier, - }); + }) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$ViewStateController', + object: this, + ); + } + } void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } viewStateNotifier.dispose(); } } diff --git a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart index 517c99002..25533e096 100644 --- a/lib/widgets/viewer/visual/video/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart @@ -153,13 +153,14 @@ class VideoSubtitles extends StatelessWidget { var transform = Matrix4.identity(); if (position != null) { - final para = RenderParagraph( + final paragraph = RenderParagraph( TextSpan(children: spans), textDirection: TextDirection.ltr, textScaleFactor: MediaQuery.textScaleFactorOf(context), )..layout(const BoxConstraints()); - final textWidth = para.getMaxIntrinsicWidth(double.infinity); - final textHeight = para.getMaxIntrinsicHeight(double.infinity); + final textWidth = paragraph.getMaxIntrinsicWidth(double.infinity); + final textHeight = paragraph.getMaxIntrinsicHeight(double.infinity); + paragraph.dispose(); late double anchorOffsetX, anchorOffsetY; switch (textHAlign) { diff --git a/plugins/aves_magnifier/lib/src/controller/controller.dart b/plugins/aves_magnifier/lib/src/controller/controller.dart index e57d35c10..24fb27ba3 100644 --- a/plugins/aves_magnifier/lib/src/controller/controller.dart +++ b/plugins/aves_magnifier/lib/src/controller/controller.dart @@ -4,6 +4,7 @@ import 'package:aves_magnifier/src/controller/state.dart'; import 'package:aves_magnifier/src/scale/scale_boundaries.dart'; import 'package:aves_magnifier/src/scale/scale_level.dart'; import 'package:aves_magnifier/src/scale/state.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class AvesMagnifierController { @@ -19,6 +20,13 @@ class AvesMagnifierController { AvesMagnifierController({ MagnifierState? initialState, }) : super() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AvesMagnifierController', + object: this, + ); + } const source = ChangeSource.internal; initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source); previousState = initial; @@ -31,6 +39,16 @@ class AvesMagnifierController { _setScaleState(_initialScaleState); } + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + _disposed = true; + _stateStreamController.close(); + _scaleBoundariesStreamController.close(); + _scaleStateChangeStreamController.close(); + } + Stream get stateStream => _stateStreamController.stream; Stream get scaleBoundariesStream => _scaleBoundariesStreamController.stream; @@ -51,13 +69,6 @@ class AvesMagnifierController { bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut; - void dispose() { - _disposed = true; - _stateStreamController.close(); - _scaleBoundariesStreamController.close(); - _scaleStateChangeStreamController.close(); - } - void update({ Offset? position, double? scale, diff --git a/plugins/aves_map/lib/src/controller.dart b/plugins/aves_map/lib/src/controller.dart index 7f853c2a8..a321057d3 100644 --- a/plugins/aves_map/lib/src/controller.dart +++ b/plugins/aves_map/lib/src/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves_map/src/zoomed_bounds.dart'; +import 'package:flutter/foundation.dart'; import 'package:latlong2/latlong.dart'; class AvesMapController { @@ -16,7 +17,20 @@ class AvesMapController { Stream get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast(); + AvesMapController() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AvesMapController', + object: this, + ); + } + } + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _streamController.close(); } diff --git a/plugins/aves_video/lib/src/controller.dart b/plugins/aves_video/lib/src/controller.dart index 7fb227629..ae1caf801 100644 --- a/plugins/aves_video/lib/src/controller.dart +++ b/plugins/aves_video/lib/src/controller.dart @@ -29,11 +29,21 @@ abstract class AvesVideoController { required this.playbackStateHandler, required this.settings, }) : _entry = entry { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AvesVideoController', + object: this, + ); + } entry.visualChangeNotifier.addListener(onVisualChanged); } @mustCallSuper Future dispose() async { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _entry.visualChangeNotifier.removeListener(onVisualChanged); await _savePlaybackState(); } diff --git a/plugins/aves_video_ijk/lib/src/controller.dart b/plugins/aves_video_ijk/lib/src/controller.dart index 56915091c..b13417263 100644 --- a/plugins/aves_video_ijk/lib/src/controller.dart +++ b/plugins/aves_video_ijk/lib/src/controller.dart @@ -85,6 +85,7 @@ class IjkVideoController extends AvesVideoController { await _valueStreamController.close(); await _timedTextStreamController.close(); await _instance.release(); + _completedNotifier.dispose(); } void _startListening() { diff --git a/plugins/aves_video_mpv/lib/src/controller.dart b/plugins/aves_video_mpv/lib/src/controller.dart index 457cda6cf..bc2c78751 100644 --- a/plugins/aves_video_mpv/lib/src/controller.dart +++ b/plugins/aves_video_mpv/lib/src/controller.dart @@ -69,6 +69,7 @@ class MpvVideoController extends AvesVideoController { await _statusStreamController.close(); await _timedTextStreamController.close(); await _instance.dispose(); + _completedNotifier.dispose(); } void _startListening() { diff --git a/pubspec.lock b/pubspec.lock index 2e1596324..cf31fc33b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -735,6 +735,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.0" + leak_tracker: + dependency: "direct main" + description: + name: leak_tracker + sha256: b63ca5cc296c7509d71f6d4a8cb6085eec8461970c503f3ef3c5c541bc3f0a9a + url: "https://pub.dev" + source: hosted + version: "9.0.6" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 32b82c153..29bf1016a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -83,6 +83,7 @@ dependencies: get_it: intl: latlong2: + leak_tracker: local_auth: material_color_utilities: material_design_icons_flutter: