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/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));
}

View file

@ -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,

View file

@ -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();

View file

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

View file

@ -99,6 +99,8 @@ class CollectionLens with ChangeNotifier {
..forEach((sub) => sub.cancel())
..clear();
favourites.removeListener(_onFavouritesChanged);
filterChangeNotifier.dispose();
sortSectionChangeNotifier.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/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();

View file

@ -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);

View file

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

View file

@ -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;

View file

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

View file

@ -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);
}

View file

@ -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) {

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

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

View file

@ -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) {

View file

@ -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();
}

View file

@ -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();

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/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)'),

View file

@ -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;

View file

@ -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();
}

View file

@ -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) {

View file

@ -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;

View file

@ -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,
);
},

View file

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

View file

@ -271,7 +271,6 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
),
),
);
mapCollection.dispose();
}
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_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();
}

View file

@ -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();

View file

@ -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();
}

View file

@ -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);

View file

@ -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) {

View file

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

View file

@ -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();
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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) {

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_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,

View file

@ -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();
}

View file

@ -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();
}

View file

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

View file

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

View file

@ -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:

View file

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