diff --git a/lib/model/source/analysis_controller.dart b/lib/model/source/analysis_controller.dart index f6996674c..b36c2306e 100644 --- a/lib/model/source/analysis_controller.dart +++ b/lib/model/source/analysis_controller.dart @@ -4,7 +4,8 @@ class AnalysisController { final bool canStartService, force; final int progressTotal, progressOffset; final List? entryIds; - final ValueNotifier stopSignal; + + final ValueNotifier _stopSignal = ValueNotifier(false); AnalysisController({ this.canStartService = true, @@ -12,8 +13,24 @@ class AnalysisController { this.force = false, this.progressTotal = 0, this.progressOffset = 0, - ValueNotifier? stopSignal, - }) : stopSignal = stopSignal ?? ValueNotifier(false); + }) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$AnalysisController', + object: this, + ); + } + } - bool get isStopping => stopSignal.value; + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + _stopSignal.dispose(); + } + + bool get isStopping => _stopSignal.value; + + void enableStopSignal() => _stopSignal.value = true; } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 385131a9f..75c4f1091 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -61,6 +61,13 @@ mixin SourceBase { abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin { CollectionSource() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'aves', + className: '$CollectionSource', + object: this, + ); + } settings.updateStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => invalidateAlbumDisplayNames()); settings.updateStream.where((event) => event.key == SettingKeys.hiddenFiltersKey).listen((event) { final oldValue = event.oldValue; @@ -76,6 +83,14 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place }); } + @mustCallSuper + void dispose() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } + _rawEntries.forEach((v) => v.dispose()); + } + final EventBus _eventBus = EventBus(); @override @@ -447,7 +462,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place Future analyze(AnalysisController? analysisController, {Set? entries}) async { final todoEntries = entries ?? visibleEntries; - final _analysisController = analysisController ?? AnalysisController(); + final defaultAnalysisController = AnalysisController(); + final _analysisController = analysisController ?? defaultAnalysisController; final force = _analysisController.force; if (!_analysisController.isStopping) { var startAnalysisService = false; @@ -481,6 +497,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place updateDerivedFilters(todoEntries); } } + defaultAnalysisController.dispose(); state = SourceState.ready; } diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 031dc54b6..d0f1db0cb 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -109,9 +109,11 @@ class Analyzer { if (kFlutterMemoryAllocationsEnabled) { MemoryAllocations.instance.dispatchObjectDisposed(object: this); } + _stopUpdateTimer(); + _controller?.dispose(); _serviceStateNotifier.removeListener(_onServiceStateChanged); _source.stateNotifier.removeListener(_onSourceStateChanged); - _stopUpdateTimer(); + _source.dispose(); } Future start(dynamic args) async { @@ -125,13 +127,13 @@ class Analyzer { progressOffset = args['progressOffset']; } debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal'); + _controller?.dispose(); _controller = AnalysisController( canStartService: false, entryIds: entryIds, force: force, progressTotal: progressTotal, progressOffset: progressOffset, - stopSignal: ValueNotifier(false), ); settings.systemLocalesFallback = await deviceService.getLocales(); @@ -160,7 +162,7 @@ class Analyzer { await _stopPlatformService(); _serviceStateNotifier.value = AnalyzerState.stopped; case AnalyzerState.stopped: - _controller?.stopSignal.value = true; + _controller?.enableStopSignal(); _stopUpdateTimer(); } } diff --git a/lib/widget_common.dart b/lib/widget_common.dart index 4197f0311..caca41659 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -90,5 +90,6 @@ Future _getWidgetEntry(int widgetId, bool reuseEntry) async { if (entry != null) { settings.setWidgetUri(widgetId, entry.uri); } + source.dispose(); return entry; } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 526d9a017..e21c30380 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -196,13 +196,14 @@ class _AvesAppState extends State with WidgetsBindingObserver { @override void dispose() { - _pageTransitionsBuilderNotifier.dispose(); - _tvMediaQueryModifierNotifier.dispose(); - _appModeNotifier.dispose(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); WidgetsBinding.instance.removeObserver(this); + _pageTransitionsBuilderNotifier.dispose(); + _tvMediaQueryModifierNotifier.dispose(); + _appModeNotifier.dispose(); + _mediaStoreSource.dispose(); super.dispose(); } diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index f5e764670..d0254b809 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -257,7 +257,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware final controller = AnalysisController(canStartService: true, force: true); final collection = context.read(); - collection.source.analyze(controller, entries: entries); + collection.source.analyze(controller, entries: entries).then((_) => controller.dispose()); _browse(context); } diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index bfcaa6b4d..e7c74fce7 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -89,9 +89,11 @@ abstract class ChooserQuickButtonState, U> exten } void _clearChooserOverlayEntry() { - if (_chooserOverlayEntry != null) { - _chooserOverlayEntry!.remove(); - _chooserOverlayEntry = null; + final overlayEntry = _chooserOverlayEntry; + _chooserOverlayEntry = null; + if (overlayEntry != null) { + overlayEntry.remove(); + overlayEntry.dispose(); } } diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 9f4493a61..5a68ec6e0 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -180,9 +180,12 @@ class _GridScaleGestureDetectorState extends State with _startDbReport(); } + @override + void dispose() { + _disposeLoadedContent(); + super.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); @@ -63,7 +69,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.reset().then((_) => _startDbReport()), + onPressed: () => metadataDb.reset().then((_) => _reload()), child: const Text('Reset'), ), ], @@ -86,7 +92,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearEntries().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearEntries().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -107,7 +113,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearDates().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -128,7 +134,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearCatalogMetadata().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -149,7 +155,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearAddresses().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -170,7 +176,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearTrashDetails().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearTrashDetails().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -191,7 +197,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => vaults.clear().then((_) => _startDbReport()), + onPressed: () => vaults.clear().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -212,7 +218,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => favourites.clear().then((_) => _startDbReport()), + onPressed: () => favourites.clear().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -233,7 +239,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => covers.clear().then((_) => _startDbReport()), + onPressed: () => covers.clear().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -254,7 +260,7 @@ class _DebugAppDatabaseSectionState extends State with ), const SizedBox(width: 8), ElevatedButton( - onPressed: () => metadataDb.clearVideoPlayback().then((_) => _startDbReport()), + onPressed: () => metadataDb.clearVideoPlayback().then((_) => _reload()), child: const Text('Clear'), ), ], @@ -268,6 +274,11 @@ class _DebugAppDatabaseSectionState extends State with ); } + Future _reload() async { + await _disposeLoadedContent(); + _startDbReport(); + } + void _startDbReport() { _dbFileSizeLoader = metadataDb.dbFileSize(); _dbEntryLoader = metadataDb.loadEntries(); @@ -281,6 +292,10 @@ class _DebugAppDatabaseSectionState extends State with _dbVideoPlaybackLoader = metadataDb.loadAllVideoPlayback(); setState(() {}); } + + Future _disposeLoadedContent() async { + (await _dbEntryLoader).forEach((v) => v.dispose()); + } @override bool get wantKeepAlive => true; diff --git a/lib/widgets/debug/general.dart b/lib/widgets/debug/general.dart index 2cabc8800..0cfd32ae4 100644 --- a/lib/widgets/debug/general.dart +++ b/lib/widgets/debug/general.dart @@ -47,14 +47,17 @@ class _DebugGeneralSectionState extends State with Automati SwitchListTile( value: _taskQueueOverlayEntry != null, onChanged: (v) { - _taskQueueOverlayEntry?.remove(); + final overlayEntry = _taskQueueOverlayEntry; + _taskQueueOverlayEntry = null; + if (overlayEntry != null) { + overlayEntry.remove(); + overlayEntry.dispose(); + } if (v) { _taskQueueOverlayEntry = OverlayEntry( builder: (context) => const DebugTaskQueueOverlay(), ); Overlay.of(context).insert(_taskQueueOverlayEntry!); - } else { - _taskQueueOverlayEntry = null; } setState(() {}); }, diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 4ec2e759f..8367eeb44 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -163,7 +163,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChanged); _mapController.dispose(); - _selectedIndexNotifier.removeListener(_onThumbnailIndexChanged); + _selectedIndexNotifier.dispose(); regionCollection?.dispose(); // provided collection should be a new instance specifically created // for the `MapPage` widget, so it can be safely disposed here diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index 01f9b57c0..de3936e50 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -49,7 +49,7 @@ class _ViewerThumbnailPreviewState extends State { @override void dispose() { - _entryIndexNotifier.removeListener(_onScrollerIndexChanged); + _entryIndexNotifier.dispose(); super.dispose(); } diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index 3f8279258..aa75ab20c 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -33,11 +33,10 @@ class VideoConductor { if (kFlutterMemoryAllocationsEnabled) { MemoryAllocations.instance.dispatchObjectDisposed(object: this); } - await _disposeAll(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - _controllers.forEach((v) => v.dispose()); + await _disposeAll(); _controllers.clear(); if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { await windowService.keepScreenOn(false); diff --git a/lib/widgets/viewer/view/conductor.dart b/lib/widgets/viewer/view/conductor.dart index 529b723ed..9cfa8fdf7 100644 --- a/lib/widgets/viewer/view/conductor.dart +++ b/lib/widgets/viewer/view/conductor.dart @@ -73,6 +73,10 @@ class ViewStateConductor { entry, ...?entry.burstEntries, }.map((v) => v.uri).toSet(); - _controllers.removeWhere((v) => uris.contains(v.entry.uri)); + final entryControllers = _controllers.where((v) => uris.contains(v.entry.uri)).toSet(); + entryControllers.forEach((controller) { + _controllers.remove(controller); + controller.dispose(); + }); } } diff --git a/lib/widgets/viewer/view/controller.dart b/lib/widgets/viewer/view/controller.dart index fcbcb6a5d..5b7451ce0 100644 --- a/lib/widgets/viewer/view/controller.dart +++ b/lib/widgets/viewer/view/controller.dart @@ -29,5 +29,6 @@ class ViewStateController with HistogramMixin { MemoryAllocations.instance.dispatchObjectDisposed(object: this); } viewStateNotifier.dispose(); + fullImageNotifier.dispose(); } } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 5ab65e474..f483bfe01 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -84,6 +84,7 @@ class _EntryPageViewState extends State with SingleTickerProvider void dispose() { _unregisterWidget(widget); widget.onDisposed?.call(); + _actionFeedbackChildNotifier.dispose(); super.dispose(); } @@ -305,9 +306,11 @@ class _EntryPageViewState extends State with SingleTickerProvider return true; }; onScaleEnd = (details) { - if (_actionFeedbackOverlayEntry != null) { - _actionFeedbackOverlayEntry!.remove(); - _actionFeedbackOverlayEntry = null; + final overlayEntry = _actionFeedbackOverlayEntry; + _actionFeedbackOverlayEntry = null; + if (overlayEntry != null) { + overlayEntry.remove(); + overlayEntry.dispose(); } }; } diff --git a/lib/widgets/viewer/visual/video/cover.dart b/lib/widgets/viewer/visual/video/cover.dart index 0d6cb9aca..3a09a897f 100644 --- a/lib/widgets/viewer/visual/video/cover.dart +++ b/lib/widgets/viewer/visual/video/cover.dart @@ -79,6 +79,7 @@ class _VideoCoverState extends State { @override void dispose() { _unregisterWidget(widget); + _videoCoverInfoNotifier.dispose(); super.dispose(); } diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart index eac8c8e18..c737108a5 100644 --- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart +++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart @@ -37,6 +37,12 @@ class MagnifierGestureDetector extends StatefulWidget { class _MagnifierGestureDetectorState extends State { final ValueNotifier doubleTapDetails = ValueNotifier(null); + @override + void dispose() { + doubleTapDetails.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final gestureSettings = MediaQuery.gestureSettingsOf(context); diff --git a/plugins/aves_video_mpv/lib/src/controller.dart b/plugins/aves_video_mpv/lib/src/controller.dart index bc2c78751..5703c8fef 100644 --- a/plugins/aves_video_mpv/lib/src/controller.dart +++ b/plugins/aves_video_mpv/lib/src/controller.dart @@ -12,7 +12,7 @@ import 'package:media_kit_video/media_kit_video.dart'; class MpvVideoController extends AvesVideoController { late Player _instance; late VideoStatus _status; - bool _firstFrameRendered = false; + bool _disposed = false, _firstFrameRendered = false; final ValueNotifier _controllerNotifier = ValueNotifier(null); final List _subscriptions = []; final StreamController _statusStreamController = StreamController.broadcast(); @@ -63,12 +63,15 @@ class MpvVideoController extends AvesVideoController { @override Future dispose() async { + assert(!_disposed); + _disposed = true; await super.dispose(); _stopListening(); _stopStreamFetchTimer(); await _statusStreamController.close(); await _timedTextStreamController.close(); await _instance.dispose(); + _controllerNotifier.dispose(); _completedNotifier.dispose(); }