memory leak tracking & fixes

This commit is contained in:
Thibault Deckers 2023-10-20 00:59:37 +03:00
parent 300d232b9b
commit 4c07a9da43
44 changed files with 270 additions and 45 deletions

View file

@ -3,7 +3,9 @@ import 'dart:isolate';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:leak_tracker/leak_tracker.dart';
void mainCommon(AppFlavor flavor, {Map? debugIntentData}) { void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
// HttpClient.enableTimelineLogging = true; // enable network traffic logging // HttpClient.enableTimelineLogging = true; // enable network traffic logging
@ -35,5 +37,9 @@ void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
// ErrorWidget.builder = (details) => ErrorWidget(details.exception); // ErrorWidget.builder = (details) => ErrorWidget(details.exception);
// cf https://docs.flutter.dev/testing/errors // cf https://docs.flutter.dev/testing/errors
LeakTracking.start();
MemoryAllocations.instance.addListener(
(event) => LeakTracking.dispatchObjectEvent(event.toMap()),
);
runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData)); runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
} }

View file

@ -347,6 +347,11 @@ class Dependencies {
licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE', 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', 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( Dependency(
name: 'Path', name: 'Path',
license: bsd3, license: bsd3,

View file

@ -49,8 +49,7 @@ class AvesEntry with AvesEntryBase {
@override @override
final AChangeNotifier visualChangeNotifier = AChangeNotifier(); final AChangeNotifier visualChangeNotifier = AChangeNotifier();
final AChangeNotifier metadataChangeNotifier = AChangeNotifier(); final AChangeNotifier metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
final AChangeNotifier addressChangeNotifier = AChangeNotifier();
AvesEntry({ AvesEntry({
required int? id, required int? id,
@ -72,6 +71,13 @@ class AvesEntry with AvesEntryBase {
required this.origin, required this.origin,
this.burstEntries, this.burstEntries,
}) : id = id ?? 0 { }) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesEntry',
object: this,
);
}
this.path = path; this.path = path;
this.sourceTitle = sourceTitle; this.sourceTitle = sourceTitle;
this.dateModifiedSecs = dateModifiedSecs; this.dateModifiedSecs = dateModifiedSecs;
@ -181,6 +187,9 @@ class AvesEntry with AvesEntryBase {
} }
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
visualChangeNotifier.dispose(); visualChangeNotifier.dispose();
metadataChangeNotifier.dispose(); metadataChangeNotifier.dispose();
addressChangeNotifier.dispose(); addressChangeNotifier.dispose();

View file

@ -16,6 +16,12 @@ class Query extends ChangeNotifier {
} }
} }
@override
void dispose() {
_focusRequestNotifier.dispose();
super.dispose();
}
bool _enabled = false; bool _enabled = false;
bool get enabled => _enabled; bool get enabled => _enabled;

View file

@ -99,6 +99,8 @@ class CollectionLens with ChangeNotifier {
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();
favourites.removeListener(_onFavouritesChanged); favourites.removeListener(_onFavouritesChanged);
filterChangeNotifier.dispose();
sortSectionChangeNotifier.dispose();
super.dispose(); super.dispose();
} }

View file

@ -10,6 +10,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -92,12 +93,22 @@ class Analyzer {
Analyzer() { Analyzer() {
debugPrint('$runtimeType create'); debugPrint('$runtimeType create');
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$Analyzer',
object: this,
);
}
_serviceStateNotifier.addListener(_onServiceStateChanged); _serviceStateNotifier.addListener(_onServiceStateChanged);
_source.stateNotifier.addListener(_onSourceStateChanged); _source.stateNotifier.addListener(_onSourceStateChanged);
} }
void dispose() { void dispose() {
debugPrint('$runtimeType dispose'); debugPrint('$runtimeType dispose');
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_serviceStateNotifier.removeListener(_onServiceStateChanged); _serviceStateNotifier.removeListener(_onServiceStateChanged);
_source.stateNotifier.removeListener(_onSourceStateChanged); _source.stateNotifier.removeListener(_onSourceStateChanged);
_stopUpdateTimer(); _stopUpdateTimer();

View file

@ -32,6 +32,12 @@ class _LicensesState extends State<Licenses> {
_sortPackages(); _sortPackages();
} }
@override
void dispose() {
_expandedNotifier.dispose();
super.dispose();
}
void _sortPackages() { void _sortPackages() {
int compare(Dependency a, Dependency b) => compareAsciiUpperCase(a.name, b.name); int compare(Dependency a, Dependency b) => compareAsciiUpperCase(a.name, b.name);
_platform.sort(compare); _platform.sort(compare);

View file

@ -644,7 +644,6 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
builder: (context) => MapPage(collection: mapCollection), builder: (context) => MapPage(collection: mapCollection),
), ),
); );
mapCollection.dispose();
} }
void _goToSlideshow(BuildContext context) { void _goToSlideshow(BuildContext context) {

View file

@ -32,20 +32,21 @@ class EntryListDetailsTheme extends StatelessWidget {
final titleStyle = textTheme.bodyMedium!; final titleStyle = textTheme.bodyMedium!;
final captionStyle = textTheme.bodySmall!; final captionStyle = textTheme.bodySmall!;
final titleLineHeight = (RenderParagraph( final titleLineHeightParagraph = RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle), TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true)) )..layout(const BoxConstraints(), parentUsesSize: true);
.getMaxIntrinsicHeight(double.infinity); final titleLineHeight = titleLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);
titleLineHeightParagraph.dispose();
final captionLineHeight = (RenderParagraph( final captionLineHeightParagraph = RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
strutStyle: AStyles.overflowStrut, strutStyle: AStyles.overflowStrut,
)..layout(const BoxConstraints(), parentUsesSize: true)) )..layout(const BoxConstraints(), parentUsesSize: true);
.getMaxIntrinsicHeight(double.infinity); final captionLineHeight = captionLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);
var titleMaxLines = 1; var titleMaxLines = 1;
var showDate = false; var showDate = false;

View file

@ -49,6 +49,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
void dispose() { void dispose() {
_animationController?.dispose(); _animationController?.dispose();
_clearChooserOverlayEntry(); _clearChooserOverlayEntry();
_chooserValueNotifier.dispose();
super.dispose(); super.dispose();
} }

View file

@ -114,14 +114,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
} }
Size textSize(String text) { Size textSize(String text) {
final para = RenderParagraph( final paragraph = RenderParagraph(
TextSpan(text: text, style: widget.textStyle), TextSpan(text: text, style: widget.textStyle),
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
strutStyle: widget.strutStyle, strutStyle: widget.strutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true); )..layout(const BoxConstraints(), parentUsesSize: true);
final width = para.getMaxIntrinsicWidth(double.infinity); final width = paragraph.getMaxIntrinsicWidth(double.infinity);
final height = para.getMaxIntrinsicHeight(double.infinity); final height = paragraph.getMaxIntrinsicHeight(double.infinity);
paragraph.dispose();
return Size(width, height); return Size(width, height);
} }

View file

@ -44,6 +44,7 @@ class TextBackgroundPainter extends StatelessWidget {
TextSelection(baseOffset: 0, extentOffset: textLength), TextSelection(baseOffset: 0, extentOffset: textLength),
boxHeightStyle: ui.BoxHeightStyle.max, boxHeightStyle: ui.BoxHeightStyle.max,
); );
paragraph.dispose();
// merge boxes to avoid artifacts at box edges, from anti-aliasing and rounding hacks // merge boxes to avoid artifacts at box edges, from anti-aliasing and rounding hacks
final lineRects = groupBy<TextBox, double>(allBoxes, (v) => v.top).entries.map((kv) { final lineRects = groupBy<TextBox, double>(allBoxes, (v) => v.top).entries.map((kv) {

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
@ -10,7 +11,20 @@ class DoubleBackPopHandler {
bool _backOnce = false; bool _backOnce = false;
Timer? _backTimer; Timer? _backTimer;
DoubleBackPopHandler() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$DoubleBackPopHandler',
object: this,
);
}
}
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_stopBackTimer(); _stopBackTimer();
} }

View file

@ -127,7 +127,7 @@ class SectionHeader<T> extends StatelessWidget {
}) { }) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context); final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final maxContentWidth = maxWidth - (SectionHeader.padding.horizontal + SectionHeader.margin.horizontal); final maxContentWidth = maxWidth - (SectionHeader.padding.horizontal + SectionHeader.margin.horizontal);
final para = RenderParagraph( final paragraph = RenderParagraph(
TextSpan( TextSpan(
children: [ children: [
// as of Flutter v3.7.7, `RenderParagraph` fails to lay out `WidgetSpan` offscreen // as of Flutter v3.7.7, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
@ -148,7 +148,9 @@ class SectionHeader<T> extends StatelessWidget {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
)..layout(BoxConstraints(maxWidth: maxContentWidth), parentUsesSize: true); )..layout(BoxConstraints(maxWidth: maxContentWidth), parentUsesSize: true);
return para.getMaxIntrinsicHeight(maxContentWidth); final height = paragraph.getMaxIntrinsicHeight(maxContentWidth);
paragraph.dispose();
return height;
} }
} }

View file

@ -38,13 +38,14 @@ class CaptionedButton extends StatefulWidget {
final width = getWidth(context); final width = getWidth(context);
var height = width; var height = width;
if (showCaption) { if (showCaption) {
final para = RenderParagraph( final paragraph = RenderParagraph(
TextSpan(text: text, style: CaptionedButtonText.textStyle(context)), TextSpan(text: text, style: CaptionedButtonText.textStyle(context)),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
maxLines: CaptionedButtonText.maxLines, maxLines: CaptionedButtonText.maxLines,
)..layout(const BoxConstraints(), parentUsesSize: true); )..layout(const BoxConstraints(), parentUsesSize: true);
height += para.getMaxIntrinsicHeight(width) + padding.vertical; height += paragraph.getMaxIntrinsicHeight(width) + padding.vertical;
paragraph.dispose();
} }
return Size(width, height); return Size(width, height);
} }

View file

@ -113,6 +113,7 @@ class _GeoMapState extends State<GeoMap> {
@override @override
void dispose() { void dispose() {
_clusterChangeNotifier.dispose();
_unregisterWidget(widget); _unregisterWidget(widget);
super.dispose(); super.dispose();
} }

View file

@ -2,6 +2,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/search/route.dart'; import 'package:aves/widgets/common/search/route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -15,11 +16,22 @@ abstract class AvesSearchDelegate extends SearchDelegate {
String? initialQuery, String? initialQuery,
required super.searchFieldLabel, required super.searchFieldLabel,
}) { }) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesSearchDelegate',
object: this,
);
}
query = initialQuery ?? ''; query = initialQuery ?? '';
} }
@mustCallSuper @mustCallSuper
void dispose() {} void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
}
@override @override
Widget? buildLeading(BuildContext context) { Widget? buildLeading(BuildContext context) {

View file

@ -88,6 +88,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
} }
void _registerWidget(ThumbnailImage widget) { void _registerWidget(ThumbnailImage widget) {
// TODO TLAD [leak] `widget.entry.visualChangeNotifier`
widget.entry.visualChangeNotifier.addListener(_onVisualChanged); widget.entry.visualChangeNotifier.addListener(_onVisualChanged);
_initProvider(); _initProvider();
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -26,6 +27,13 @@ class TileExtentController {
required this.spacing, required this.spacing,
required this.horizontalPadding, 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 // initialize extent to 0, so that it will be dynamically sized on first launch
extentNotifier = ValueNotifier(0); extentNotifier = ValueNotifier(0);
userPreferredExtent = settings.getTileExtent(settingsRouteKey); userPreferredExtent = settings.getTileExtent(settingsRouteKey);
@ -33,6 +41,9 @@ class TileExtentController {
} }
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_subscriptions _subscriptions
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();

View file

@ -27,9 +27,11 @@ import 'package:aves/widgets/debug/report.dart';
import 'package:aves/widgets/debug/settings.dart'; import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart'; import 'package:aves/widgets/debug/storage.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:leak_tracker/leak_tracker.dart';
class AppDebugPage extends StatefulWidget { class AppDebugPage extends StatefulWidget {
static const routeName = '/debug'; static const routeName = '/debug';
@ -133,6 +135,23 @@ class _AppDebugPageState extends State<AppDebugPage> {
}, },
title: const Text('Show tasks overlay'), 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( ElevatedButton(
onPressed: () => source.init(loadTopEntriesFirst: false), onPressed: () => source.init(loadTopEntriesFirst: false),
child: const Text('Source refresh (top off)'), child: const Text('Source refresh (top off)'),

View file

@ -437,12 +437,13 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
l10n.setCoverDialogCustom, l10n.setCoverDialogCustom,
}.fold('', (previousValue, element) => '$previousValue\n$element'); }.fold('', (previousValue, element) => '$previousValue\n$element');
final para = RenderParagraph( final paragraph = RenderParagraph(
TextSpan(text: _optionLines, style: Theme.of(context).textTheme.titleMedium!), TextSpan(text: _optionLines, style: Theme.of(context).textTheme.titleMedium!),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
)..layout(const BoxConstraints(), parentUsesSize: true); )..layout(const BoxConstraints(), parentUsesSize: true);
final textWidth = para.getMaxIntrinsicWidth(double.infinity); final textWidth = paragraph.getMaxIntrinsicWidth(double.infinity);
paragraph.dispose();
// from `RadioListTile` layout // from `RadioListTile` layout
const contentPadding = 32; const contentPadding = 32;

View file

@ -33,11 +33,21 @@ class TransformController {
final Size displaySize; final Size displaySize;
TransformController(this.displaySize) { TransformController(this.displaySize) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$TransformController',
object: this,
);
}
reset(); reset();
aspectRatioNotifier.addListener(_onAspectRatioChanged); aspectRatioNotifier.addListener(_onAspectRatioChanged);
} }
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
aspectRatioNotifier.dispose(); aspectRatioNotifier.dispose();
} }

View file

@ -255,7 +255,6 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
builder: (context) => MapPage(collection: mapCollection), builder: (context) => MapPage(collection: mapCollection),
), ),
); );
mapCollection.dispose();
} }
void _goToSlideshow(BuildContext context, Set<T> filters) { void _goToSlideshow(BuildContext context, Set<T> filters) {

View file

@ -38,20 +38,22 @@ class FilterListDetailsTheme extends StatelessWidget {
final captionStyle = textTheme.bodySmall!; final captionStyle = textTheme.bodySmall!;
final titleIconSize = AvesFilterChip.iconSize * textScaleFactor; final titleIconSize = AvesFilterChip.iconSize * textScaleFactor;
final titleLineHeight = (RenderParagraph( final titleLineHeightParagraph = RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle), TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true)) )..layout(const BoxConstraints(), parentUsesSize: true);
.getMaxIntrinsicHeight(double.infinity); final titleLineHeight = titleLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);
titleLineHeightParagraph.dispose();
final captionLineHeight = (RenderParagraph( final captionLineHeightParagraph = RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle), TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
strutStyle: AStyles.overflowStrut, strutStyle: AStyles.overflowStrut,
)..layout(const BoxConstraints(), parentUsesSize: true)) )..layout(const BoxConstraints(), parentUsesSize: true);
.getMaxIntrinsicHeight(double.infinity); final captionLineHeight = captionLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);
captionLineHeightParagraph.dispose();
var titleMaxLines = 1; var titleMaxLines = 1;
var showCount = false; var showCount = false;

View file

@ -165,6 +165,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
_mapController.dispose(); _mapController.dispose();
_selectedIndexNotifier.removeListener(_onThumbnailIndexChanged); _selectedIndexNotifier.removeListener(_onThumbnailIndexChanged);
regionCollection?.dispose(); 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(); super.dispose();
} }
@ -394,10 +397,11 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
TransparentMaterialPageRoute( TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName), settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) { pageBuilder: (context, a, sa) {
return EntryViewerPage( final viewerCollection = regionCollection?.copyWith(
collection: regionCollection?.copyWith(
listenToSource: false, listenToSource: false,
), );
return EntryViewerPage(
collection: viewerCollection,
initialEntry: initialEntry, initialEntry: initialEntry,
); );
}, },

View file

@ -104,6 +104,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
@override @override
void dispose() { void dispose() {
_quickActionsChangeNotifier.dispose();
_stopLeavingTimer(); _stopLeavingTimer();
super.dispose(); super.dispose();
} }

View file

@ -271,7 +271,6 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
), ),
), ),
); );
mapCollection.dispose();
} }
void _goToDebug(BuildContext context, AvesEntry targetEntry) { void _goToDebug(BuildContext context, AvesEntry targetEntry) {

View file

@ -23,6 +23,7 @@ import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:aves_video/aves_video.dart'; import 'package:aves_video/aves_video.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -32,9 +33,20 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
VideoActionDelegate({ VideoActionDelegate({
required this.collection, required this.collection,
}); }) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$VideoActionDelegate',
object: this,
);
}
}
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
stopOverlayHidingTimer(); stopOverlayHidingTimer();
} }

View file

@ -6,6 +6,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/controls/events.dart'; import 'package:aves/widgets/viewer/controls/events.dart';
import 'package:aves_magnifier/aves_magnifier.dart'; import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class ViewerController { class ViewerController {
@ -47,6 +48,13 @@ class ViewerController {
this.autopilotInterval, this.autopilotInterval,
this.autopilotAnimatedZoom = false, this.autopilotAnimatedZoom = false,
}) { }) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$ViewerController',
object: this,
);
}
_initialScale = initialScale; _initialScale = initialScale;
_autopilotNotifier = ValueNotifier(autopilot); _autopilotNotifier = ValueNotifier(autopilot);
_autopilotNotifier.addListener(_onAutopilotChanged); _autopilotNotifier.addListener(_onAutopilotChanged);
@ -54,6 +62,9 @@ class ViewerController {
} }
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_autopilotNotifier.removeListener(_onAutopilotChanged); _autopilotNotifier.removeListener(_onAutopilotChanged);
_clearAutopilotAnimations(); _clearAutopilotAnimations();
_stopPlayTimer(); _stopPlayTimer();

View file

@ -34,6 +34,9 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
@override @override
void dispose() { void dispose() {
_viewerController.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(); super.dispose();
} }

View file

@ -192,7 +192,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_overlayVisible.dispose(); _overlayVisible.dispose();
_viewLocked.dispose(); _viewLocked.dispose();
_overlayExpandedNotifier.dispose(); _overlayExpandedNotifier.dispose();
_currentVerticalPage.dispose();
_horizontalPager.dispose();
_verticalPager.dispose(); _verticalPager.dispose();
_verticalScrollNotifier.dispose();
_heroInfoNotifier.dispose(); _heroInfoNotifier.dispose();
_stopOverlayHidingTimer(); _stopOverlayHidingTimer();
AvesApp.lifecycleStateNotifier.removeListener(_onAppLifecycleStateChanged); AvesApp.lifecycleStateNotifier.removeListener(_onAppLifecycleStateChanged);

View file

@ -139,12 +139,14 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
} }
double _getSpanWidth(TextSpan span, double textScaleFactor) { double _getSpanWidth(TextSpan span, double textScaleFactor) {
final para = RenderParagraph( final paragraph = RenderParagraph(
span, span,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true); )..layout(const BoxConstraints(), parentUsesSize: true);
return para.getMaxIntrinsicWidth(double.infinity); final width = paragraph.getMaxIntrinsicWidth(double.infinity);
paragraph.dispose();
return width;
} }
List<InlineSpan> _buildTextValueSpans(BuildContext context, String key, String value) { List<InlineSpan> _buildTextValueSpans(BuildContext context, String key, String value) {

View file

@ -153,7 +153,6 @@ class _LocationSectionState extends State<LocationSection> {
), ),
), ),
); );
mapCollection.dispose();
} }
void _onMetadataChanged() { void _onMetadataChanged() {

View file

@ -24,6 +24,13 @@ class MultiPageController {
set page(int? page) => pageNotifier.value = page; set page(int? page) => pageNotifier.value = page;
MultiPageController(this.entry) { MultiPageController(this.entry) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$MultiPageController',
object: this,
);
}
reset(); reset();
} }
@ -40,6 +47,9 @@ class MultiPageController {
}); });
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_disposed = true; _disposed = true;
pageNotifier.dispose(); pageNotifier.dispose();
} }

View file

@ -25,6 +25,7 @@ class VideoConductor {
_subscriptions _subscriptions
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();
_controllers.forEach((v) => v.dispose());
_controllers.clear(); _controllers.clear();
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
await windowService.keepScreenOn(false); await windowService.keepScreenOn(false);

View file

@ -1,6 +1,7 @@
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/view_state.dart'; import 'package:aves/model/view_state.dart';
import 'package:aves/widgets/viewer/view/histogram.dart'; import 'package:aves/widgets/viewer/view/histogram.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ViewStateController with HistogramMixin { class ViewStateController with HistogramMixin {
@ -13,9 +14,20 @@ class ViewStateController with HistogramMixin {
ViewStateController({ ViewStateController({
required this.entry, required this.entry,
required this.viewStateNotifier, required this.viewStateNotifier,
}); }) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$ViewStateController',
object: this,
);
}
}
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
viewStateNotifier.dispose(); viewStateNotifier.dispose();
} }
} }

View file

@ -153,13 +153,14 @@ class VideoSubtitles extends StatelessWidget {
var transform = Matrix4.identity(); var transform = Matrix4.identity();
if (position != null) { if (position != null) {
final para = RenderParagraph( final paragraph = RenderParagraph(
TextSpan(children: spans), TextSpan(children: spans),
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context), textScaleFactor: MediaQuery.textScaleFactorOf(context),
)..layout(const BoxConstraints()); )..layout(const BoxConstraints());
final textWidth = para.getMaxIntrinsicWidth(double.infinity); final textWidth = paragraph.getMaxIntrinsicWidth(double.infinity);
final textHeight = para.getMaxIntrinsicHeight(double.infinity); final textHeight = paragraph.getMaxIntrinsicHeight(double.infinity);
paragraph.dispose();
late double anchorOffsetX, anchorOffsetY; late double anchorOffsetX, anchorOffsetY;
switch (textHAlign) { switch (textHAlign) {

View file

@ -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_boundaries.dart';
import 'package:aves_magnifier/src/scale/scale_level.dart'; import 'package:aves_magnifier/src/scale/scale_level.dart';
import 'package:aves_magnifier/src/scale/state.dart'; import 'package:aves_magnifier/src/scale/state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AvesMagnifierController { class AvesMagnifierController {
@ -19,6 +20,13 @@ class AvesMagnifierController {
AvesMagnifierController({ AvesMagnifierController({
MagnifierState? initialState, MagnifierState? initialState,
}) : super() { }) : super() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesMagnifierController',
object: this,
);
}
const source = ChangeSource.internal; const source = ChangeSource.internal;
initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source); initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source);
previousState = initial; previousState = initial;
@ -31,6 +39,16 @@ class AvesMagnifierController {
_setScaleState(_initialScaleState); _setScaleState(_initialScaleState);
} }
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_disposed = true;
_stateStreamController.close();
_scaleBoundariesStreamController.close();
_scaleStateChangeStreamController.close();
}
Stream<MagnifierState> get stateStream => _stateStreamController.stream; Stream<MagnifierState> get stateStream => _stateStreamController.stream;
Stream<ScaleBoundaries> get scaleBoundariesStream => _scaleBoundariesStreamController.stream; Stream<ScaleBoundaries> get scaleBoundariesStream => _scaleBoundariesStreamController.stream;
@ -51,13 +69,6 @@ class AvesMagnifierController {
bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut; bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut;
void dispose() {
_disposed = true;
_stateStreamController.close();
_scaleBoundariesStreamController.close();
_scaleStateChangeStreamController.close();
}
void update({ void update({
Offset? position, Offset? position,
double? scale, double? scale,

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves_map/src/zoomed_bounds.dart'; import 'package:aves_map/src/zoomed_bounds.dart';
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class AvesMapController { class AvesMapController {
@ -16,7 +17,20 @@ class AvesMapController {
Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>(); Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>();
AvesMapController() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesMapController',
object: this,
);
}
}
void dispose() { void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_streamController.close(); _streamController.close();
} }

View file

@ -29,11 +29,21 @@ abstract class AvesVideoController {
required this.playbackStateHandler, required this.playbackStateHandler,
required this.settings, required this.settings,
}) : _entry = entry { }) : _entry = entry {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesVideoController',
object: this,
);
}
entry.visualChangeNotifier.addListener(onVisualChanged); entry.visualChangeNotifier.addListener(onVisualChanged);
} }
@mustCallSuper @mustCallSuper
Future<void> dispose() async { Future<void> dispose() async {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_entry.visualChangeNotifier.removeListener(onVisualChanged); _entry.visualChangeNotifier.removeListener(onVisualChanged);
await _savePlaybackState(); await _savePlaybackState();
} }

View file

@ -85,6 +85,7 @@ class IjkVideoController extends AvesVideoController {
await _valueStreamController.close(); await _valueStreamController.close();
await _timedTextStreamController.close(); await _timedTextStreamController.close();
await _instance.release(); await _instance.release();
_completedNotifier.dispose();
} }
void _startListening() { void _startListening() {

View file

@ -69,6 +69,7 @@ class MpvVideoController extends AvesVideoController {
await _statusStreamController.close(); await _statusStreamController.close();
await _timedTextStreamController.close(); await _timedTextStreamController.close();
await _instance.dispose(); await _instance.dispose();
_completedNotifier.dispose();
} }
void _startListening() { void _startListening() {

View file

@ -735,6 +735,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.0" 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: lints:
dependency: transitive dependency: transitive
description: description:

View file

@ -83,6 +83,7 @@ dependencies:
get_it: get_it:
intl: intl:
latlong2: latlong2:
leak_tracker:
local_auth: local_auth:
material_color_utilities: material_color_utilities:
material_design_icons_flutter: material_design_icons_flutter: