memory leak tracking & fixes
This commit is contained in:
parent
300d232b9b
commit
4c07a9da43
44 changed files with 270 additions and 45 deletions
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -16,6 +16,12 @@ class Query extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusRequestNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _enabled = false;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
|
|
@ -99,6 +99,8 @@ class CollectionLens with ChangeNotifier {
|
|||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
favourites.removeListener(_onFavouritesChanged);
|
||||
filterChangeNotifier.dispose();
|
||||
sortSectionChangeNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -32,6 +32,12 @@ class _LicensesState extends State<Licenses> {
|
|||
_sortPackages();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_expandedNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _sortPackages() {
|
||||
int compare(Dependency a, Dependency b) => compareAsciiUpperCase(a.name, b.name);
|
||||
_platform.sort(compare);
|
||||
|
|
|
@ -644,7 +644,6 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
builder: (context) => MapPage(collection: mapCollection),
|
||||
),
|
||||
);
|
||||
mapCollection.dispose();
|
||||
}
|
||||
|
||||
void _goToSlideshow(BuildContext context) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -49,6 +49,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
|||
void dispose() {
|
||||
_animationController?.dispose();
|
||||
_clearChooserOverlayEntry();
|
||||
_chooserValueNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -114,14 +114,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TextBox, double>(allBoxes, (v) => v.top).entries.map((kv) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ class SectionHeader<T> 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<T> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ class _GeoMapState extends State<GeoMap> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_clusterChangeNotifier.dispose();
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -88,6 +88,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
|||
}
|
||||
|
||||
void _registerWidget(ThumbnailImage widget) {
|
||||
// TODO TLAD [leak] `widget.entry.visualChangeNotifier`
|
||||
widget.entry.visualChangeNotifier.addListener(_onVisualChanged);
|
||||
_initProvider();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<AppDebugPage> {
|
|||
},
|
||||
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)'),
|
||||
|
|
|
@ -437,12 +437,13 @@ class _CoverSelectionDialogState extends State<CoverSelectionDialog> {
|
|||
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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -255,7 +255,6 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
|
|||
builder: (context) => MapPage(collection: mapCollection),
|
||||
),
|
||||
);
|
||||
mapCollection.dispose();
|
||||
}
|
||||
|
||||
void _goToSlideshow(BuildContext context, Set<T> filters) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -104,6 +104,7 @@ class _QuickActionEditorBodyState<T extends Object> extends State<QuickActionEdi
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_quickActionsChangeNotifier.dispose();
|
||||
_stopLeavingTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
|
@ -271,7 +271,6 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
),
|
||||
),
|
||||
);
|
||||
mapCollection.dispose();
|
||||
}
|
||||
|
||||
void _goToDebug(BuildContext context, AvesEntry targetEntry) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import 'package:aves/widgets/viewer/controls/notifications.dart';
|
|||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:aves_video/aves_video.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -32,9 +33,20 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
|
||||
VideoActionDelegate({
|
||||
required this.collection,
|
||||
});
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$VideoActionDelegate',
|
||||
object: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
stopOverlayHidingTimer();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/widgets/viewer/controls/events.dart';
|
||||
import 'package:aves_magnifier/aves_magnifier.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ViewerController {
|
||||
|
@ -47,6 +48,13 @@ class ViewerController {
|
|||
this.autopilotInterval,
|
||||
this.autopilotAnimatedZoom = false,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$ViewerController',
|
||||
object: this,
|
||||
);
|
||||
}
|
||||
_initialScale = initialScale;
|
||||
_autopilotNotifier = ValueNotifier(autopilot);
|
||||
_autopilotNotifier.addListener(_onAutopilotChanged);
|
||||
|
@ -54,6 +62,9 @@ class ViewerController {
|
|||
}
|
||||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_autopilotNotifier.removeListener(_onAutopilotChanged);
|
||||
_clearAutopilotAnimations();
|
||||
_stopPlayTimer();
|
||||
|
|
|
@ -34,6 +34,9 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
|
|||
@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();
|
||||
}
|
||||
|
||||
|
|
|
@ -192,7 +192,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
_overlayVisible.dispose();
|
||||
_viewLocked.dispose();
|
||||
_overlayExpandedNotifier.dispose();
|
||||
_currentVerticalPage.dispose();
|
||||
_horizontalPager.dispose();
|
||||
_verticalPager.dispose();
|
||||
_verticalScrollNotifier.dispose();
|
||||
_heroInfoNotifier.dispose();
|
||||
_stopOverlayHidingTimer();
|
||||
AvesApp.lifecycleStateNotifier.removeListener(_onAppLifecycleStateChanged);
|
||||
|
|
|
@ -139,12 +139,14 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
|||
}
|
||||
|
||||
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<InlineSpan> _buildTextValueSpans(BuildContext context, String key, String value) {
|
||||
|
|
|
@ -153,7 +153,6 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
),
|
||||
),
|
||||
);
|
||||
mapCollection.dispose();
|
||||
}
|
||||
|
||||
void _onMetadataChanged() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<MagnifierState> get stateStream => _stateStreamController.stream;
|
||||
|
||||
Stream<ScaleBoundaries> 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,
|
||||
|
|
|
@ -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<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() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_streamController.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void> dispose() async {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_entry.visualChangeNotifier.removeListener(onVisualChanged);
|
||||
await _savePlaybackState();
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ class IjkVideoController extends AvesVideoController {
|
|||
await _valueStreamController.close();
|
||||
await _timedTextStreamController.close();
|
||||
await _instance.release();
|
||||
_completedNotifier.dispose();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
|
|
|
@ -69,6 +69,7 @@ class MpvVideoController extends AvesVideoController {
|
|||
await _statusStreamController.close();
|
||||
await _timedTextStreamController.close();
|
||||
await _instance.dispose();
|
||||
_completedNotifier.dispose();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -83,6 +83,7 @@ dependencies:
|
|||
get_it:
|
||||
intl:
|
||||
latlong2:
|
||||
leak_tracker:
|
||||
local_auth:
|
||||
material_color_utilities:
|
||||
material_design_icons_flutter:
|
||||
|
|
Loading…
Reference in a new issue