fix: guard lifecycle-bound state updates

This commit is contained in:
edde746
2026-07-12 19:00:28 +02:00
parent 2101f1d7f0
commit 917f6a2839
5 changed files with 44 additions and 31 deletions
+5 -6
View File
@@ -8,7 +8,7 @@ import '../services/settings_binding_owner.dart';
import '../services/settings_service.dart' as settings;
import '../theme/mono_theme.dart';
class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin, WidgetsBindingObserver {
late final SettingsBindingOwner _settingsBinding;
settings.ThemeMode _themeMode = settings.ThemeMode.system;
late Brightness _systemBrightness;
@@ -25,10 +25,11 @@ class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
onRefresh: (service) => _syncThemeMode(service.read(settings.SettingsService.themeMode)),
);
unawaited(_settingsBinding.bind());
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = _onBrightnessChanged;
WidgetsBinding.instance.addObserver(this);
}
void _onBrightnessChanged() {
@override
void didChangePlatformBrightness() {
_systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
if (_themeMode == settings.ThemeMode.system) {
safeNotifyListeners();
@@ -38,9 +39,7 @@ class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin {
@override
void dispose() {
_settingsBinding.dispose();
if (WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged == _onBrightnessChanged) {
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = null;
}
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -63,6 +63,7 @@ class _AtmosDiagnosticsScreenState extends State<AtmosDiagnosticsScreen> {
}
try {
await _channel.invokeMethod('start', {'mode': mode, if (needsUrl) 'url': url});
if (!mounted) return;
setState(() => _activeMode = mode);
} on PlatformException catch (e) {
if (mounted) showErrorSnackBar(context, e.message ?? e.code);
+25 -20
View File
@@ -14,13 +14,12 @@ class SmartDeletionHandler {
required String globalKey,
int delayMs = 500,
}) async {
bool dialogShown = false;
bool deletionComplete = false;
final dialogKey = GlobalKey();
Future.delayed(Duration(milliseconds: delayMs), () {
if (!deletionComplete && context.mounted) {
dialogShown = true;
_showProgressDialog(context, provider, globalKey);
_showProgressDialog(context, provider, globalKey, dialogKey);
}
});
@@ -28,32 +27,38 @@ class SmartDeletionHandler {
await provider.deleteDownload(globalKey);
} finally {
deletionComplete = true;
// Close dialog if shown (with canPop guard to prevent double-pop)
if (dialogShown && context.mounted && Navigator.canPop(context)) {
Navigator.of(context).pop();
final dialogContext = dialogKey.currentContext;
if (dialogContext != null && dialogContext.mounted) {
final route = ModalRoute.of(dialogContext);
if (route != null && route.isActive) {
Navigator.of(dialogContext).removeRoute(route);
}
}
}
}
static void _showProgressDialog(BuildContext context, DownloadProvider _, String globalKey) {
static void _showProgressDialog(BuildContext context, DownloadProvider _, String globalKey, GlobalKey dialogKey) {
showScopedDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => Consumer<DownloadProvider>(
builder: (context, provider, child) {
final progress = provider.getDeletionProgress(globalKey);
builder: (dialogContext) => KeyedSubtree(
key: dialogKey,
child: Consumer<DownloadProvider>(
builder: (context, provider, child) {
final progress = provider.getDeletionProgress(globalKey);
if (progress == null) {
return AlertDialog(
content: Row(
mainAxisSize: .min,
children: [const CircularProgressIndicator(), const SizedBox(width: 20), Text(t.downloads.deleting)],
),
);
}
if (progress == null) {
return AlertDialog(
content: Row(
mainAxisSize: .min,
children: [const CircularProgressIndicator(), const SizedBox(width: 20), Text(t.downloads.deleting)],
),
);
}
return DeletionProgressDialog(progress: progress);
},
return DeletionProgressDialog(progress: progress);
},
),
),
);
}
@@ -141,6 +141,7 @@ class ServerActivitiesButtonState extends State<ServerActivitiesButton> {
} catch (_) {
return;
}
if (!mounted || _overlayEntry == null) return;
_pollTimer?.cancel();
_pollTimer = null;
_panelNotifier.value = _PanelData.loading;
@@ -41,6 +41,7 @@ class PerformanceStatsService {
String _runtimePlayerType = 'unknown';
StreamSubscription<void>? _backendSwitchedSubscription;
bool _fetchInProgress = false;
bool _disposed = false;
PerformanceStatsService(this.player);
@@ -110,7 +111,7 @@ class PerformanceStatsService {
/// Fetch all performance stats from the player.
Future<void> _fetchStats() async {
if (_fetchInProgress) return;
if (_disposed || _fetchInProgress) return;
_fetchInProgress = true;
try {
// Ensure we know the runtime type on first fetch
@@ -127,7 +128,7 @@ class PerformanceStatsService {
await _fetchMpvStats();
}
} catch (e) {
appLogger.w('Failed to fetch performance stats', error: e);
if (!_disposed) appLogger.w('Failed to fetch performance stats', error: e);
} finally {
_fetchInProgress = false;
}
@@ -185,7 +186,7 @@ class PerformanceStatsService {
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
_emit(stats);
} else {
// Parse ExoPlayer stats format
final stats = PerformanceStats(
@@ -225,7 +226,7 @@ class PerformanceStatsService {
appMemoryBytes: appMemory,
uiFps: _currentUiFps,
);
_statsController.add(stats);
_emit(stats);
}
}
@@ -323,7 +324,11 @@ class PerformanceStatsService {
uiFps: _currentUiFps,
);
_statsController.add(stats);
_emit(stats);
}
void _emit(PerformanceStats stats) {
if (!_disposed && !_statsController.isClosed) _statsController.add(stats);
}
/// Parse a string to int, returning null if parsing fails.
@@ -361,6 +366,8 @@ class PerformanceStatsService {
/// Dispose of the service and release resources.
void dispose() {
if (_disposed) return;
_disposed = true;
_backendSwitchedSubscription?.cancel();
_backendSwitchedSubscription = null;
stopPolling();