From c36fee17dda4fde5668e320db3ba89380d7a7589 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:35:52 +0200 Subject: [PATCH] refactor(providers): apply DisposableChangeNotifierMixin to 11 providers --- lib/providers/companion_remote_provider.dart | 49 ++++++++++---------- lib/providers/hidden_libraries_provider.dart | 11 +++-- lib/providers/libraries_provider.dart | 13 +++--- lib/providers/offline_watch_provider.dart | 9 ++-- lib/providers/playback_state_provider.dart | 11 +++-- lib/providers/settings_provider.dart | 9 ++-- lib/providers/shader_provider.dart | 11 +++-- lib/providers/theme_provider.dart | 11 +++-- lib/providers/trackers_provider.dart | 15 +++--- lib/providers/trakt_account_provider.dart | 7 +-- lib/providers/user_profile_provider.dart | 19 ++++---- 11 files changed, 88 insertions(+), 77 deletions(-) diff --git a/lib/providers/companion_remote_provider.dart b/lib/providers/companion_remote_provider.dart index 6e020a4c..beb186a4 100644 --- a/lib/providers/companion_remote_provider.dart +++ b/lib/providers/companion_remote_provider.dart @@ -12,12 +12,13 @@ import '../services/companion_remote/lan_discovery_service.dart'; import '../services/companion_remote/remote_auth_service.dart'; import '../services/storage_service.dart'; import '../utils/app_logger.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; export '../services/companion_remote/lan_discovery_service.dart' show DiscoveredHost; typedef CommandReceivedCallback = void Function(RemoteCommand command); -class CompanionRemoteProvider with ChangeNotifier { +class CompanionRemoteProvider with ChangeNotifier, DisposableChangeNotifierMixin { RemoteSession? _session; CompanionRemotePeerService? _peerService; LanDiscoveryService? _discoveryService; @@ -97,7 +98,7 @@ class CompanionRemoteProvider with ChangeNotifier { _platform = Platform.operatingSystem; } - notifyListeners(); + safeNotifyListeners(); } /// Initialize crypto context from Plex home data. @@ -159,7 +160,7 @@ class CompanionRemoteProvider with ChangeNotifier { ); _session = RemoteSession(role: RemoteSessionRole.host, status: RemoteSessionStatus.connected); - notifyListeners(); + safeNotifyListeners(); // Start LAN discovery broadcasting _discoveryService ??= LanDiscoveryService(); @@ -181,7 +182,7 @@ class CompanionRemoteProvider with ChangeNotifier { status: RemoteSessionStatus.error, errorMessage: e.toString(), ); - notifyListeners(); + safeNotifyListeners(); } } @@ -199,7 +200,7 @@ class CompanionRemoteProvider with ChangeNotifier { _session = null; _isPlayerActive = false; _intentionalDisconnect = false; - notifyListeners(); + safeNotifyListeners(); } // ── Client: Discovery ── @@ -237,7 +238,7 @@ class CompanionRemoteProvider with ChangeNotifier { _setupPeerServiceListeners(); _session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting); - notifyListeners(); + safeNotifyListeners(); try { final winner = await _peerService!.joinSessionRacing( @@ -252,12 +253,12 @@ class CompanionRemoteProvider with ChangeNotifier { _lastHostAddresses = [winner]; _session = _session?.copyWith(status: RemoteSessionStatus.connected); - notifyListeners(); + safeNotifyListeners(); appLogger.d('CompanionRemote: Connected to ${host.name} via $winner'); } catch (e) { appLogger.e('CompanionRemote: Failed to connect to host', error: e); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString()); - notifyListeners(); + safeNotifyListeners(); rethrow; } } @@ -279,7 +280,7 @@ class CompanionRemoteProvider with ChangeNotifier { _setupPeerServiceListeners(); _session = RemoteSession(role: RemoteSessionRole.remote, status: RemoteSessionStatus.connecting); - notifyListeners(); + safeNotifyListeners(); try { await _peerService!.joinSession( @@ -293,11 +294,11 @@ class CompanionRemoteProvider with ChangeNotifier { ); _session = _session?.copyWith(status: RemoteSessionStatus.connected); - notifyListeners(); + safeNotifyListeners(); } catch (e) { appLogger.e('CompanionRemote: Failed to connect to manual host', error: e); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: e.toString()); - notifyListeners(); + safeNotifyListeners(); rethrow; } } @@ -327,25 +328,25 @@ class CompanionRemoteProvider with ChangeNotifier { _deviceConnectedSubscription = _peerService!.onDeviceConnected.listen((device) { appLogger.d('CompanionRemote: Device connected: ${device.name}'); _session = _session?.copyWith(status: RemoteSessionStatus.connected, connectedDevice: device); - notifyListeners(); + safeNotifyListeners(); }); _deviceDisconnectedSubscription = _peerService!.onDeviceDisconnected.listen((_) { appLogger.d('CompanionRemote: Device disconnected (intentional: $_intentionalDisconnect)'); if (_intentionalDisconnect) { _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true); - notifyListeners(); + safeNotifyListeners(); } else if (isHost) { _session = _session?.copyWith( status: RemoteSessionStatus.reconnecting, clearConnectedDevice: true, clearErrorMessage: true, ); - notifyListeners(); + safeNotifyListeners(); appLogger.d('CompanionRemote: Host waiting for client to reconnect'); } else { _session = _session?.copyWith(status: RemoteSessionStatus.reconnecting); - notifyListeners(); + safeNotifyListeners(); _scheduleReconnect(); } }); @@ -353,13 +354,13 @@ class CompanionRemoteProvider with ChangeNotifier { _errorSubscription = _peerService!.onError.listen((error) { appLogger.e('CompanionRemote: Error: ${error.message}'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: error.message); - notifyListeners(); + safeNotifyListeners(); }); _statusSubscription = _peerService!.onConnectionStateChanged.listen((status) { appLogger.d('CompanionRemote: Status changed: $status'); _session = _session?.copyWith(status: status); - notifyListeners(); + safeNotifyListeners(); }); } @@ -375,7 +376,7 @@ class CompanionRemoteProvider with ChangeNotifier { final device = RemoteDevice(id: id, name: name, platform: platform); _session = _session?.copyWith(connectedDevice: device); - notifyListeners(); + safeNotifyListeners(); } } @@ -383,7 +384,7 @@ class CompanionRemoteProvider with ChangeNotifier { final playerActive = command.data?['playerActive'] as bool? ?? false; if (_isPlayerActive != playerActive) { _isPlayerActive = playerActive; - notifyListeners(); + safeNotifyListeners(); } } @@ -422,7 +423,7 @@ class CompanionRemoteProvider with ChangeNotifier { errorMessage: 'Connection lost after $_maxReconnectAttempts attempts', ); _reconnectAttempts = 0; - notifyListeners(); + safeNotifyListeners(); return; } @@ -438,7 +439,7 @@ class CompanionRemoteProvider with ChangeNotifier { if (_lastHostAddresses == null || !isCryptoReady) { appLogger.w('CompanionRemote: No stored context for reconnect'); _session = _session?.copyWith(status: RemoteSessionStatus.error, errorMessage: 'Connection lost'); - notifyListeners(); + safeNotifyListeners(); return; } @@ -464,7 +465,7 @@ class CompanionRemoteProvider with ChangeNotifier { _session = _session?.copyWith(status: RemoteSessionStatus.connected, clearErrorMessage: true); _reconnectAttempts = 0; - notifyListeners(); + safeNotifyListeners(); appLogger.d('CompanionRemote: Reconnected successfully'); } catch (e) { appLogger.e('CompanionRemote: Reconnect failed', error: e); @@ -484,7 +485,7 @@ class CompanionRemoteProvider with ChangeNotifier { _reconnectTimer?.cancel(); _reconnectAttempts = 0; _session = _session?.copyWith(status: RemoteSessionStatus.disconnected, clearConnectedDevice: true); - notifyListeners(); + safeNotifyListeners(); } Future leaveSession() async { @@ -506,7 +507,7 @@ class CompanionRemoteProvider with ChangeNotifier { } _isPlayerActive = false; _intentionalDisconnect = false; - notifyListeners(); + safeNotifyListeners(); } @override diff --git a/lib/providers/hidden_libraries_provider.dart b/lib/providers/hidden_libraries_provider.dart index f6f6bdb6..2f56b204 100644 --- a/lib/providers/hidden_libraries_provider.dart +++ b/lib/providers/hidden_libraries_provider.dart @@ -1,10 +1,11 @@ import 'package:flutter/foundation.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../services/storage_service.dart'; /// Provider for managing hidden library state across the app. /// This ensures that when a library is hidden/unhidden in one screen, /// all other screens are automatically updated. -class HiddenLibrariesProvider extends ChangeNotifier { +class HiddenLibrariesProvider extends ChangeNotifier with DisposableChangeNotifierMixin { late StorageService _storageService; Set _hiddenLibraryKeys = {}; bool _isInitialized = false; @@ -31,7 +32,7 @@ class HiddenLibrariesProvider extends ChangeNotifier { _storageService = await StorageService.getInstance(); _hiddenLibraryKeys = _storageService.getHiddenLibraries(); _isInitialized = true; - notifyListeners(); + safeNotifyListeners(); } /// Hide a library by its key @@ -41,7 +42,7 @@ class HiddenLibrariesProvider extends ChangeNotifier { if (!_hiddenLibraryKeys.contains(libraryKey)) { _hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..add(libraryKey); await _storageService.saveHiddenLibraries(_hiddenLibraryKeys); - notifyListeners(); + safeNotifyListeners(); } } @@ -52,7 +53,7 @@ class HiddenLibrariesProvider extends ChangeNotifier { if (_hiddenLibraryKeys.contains(libraryKey)) { _hiddenLibraryKeys = Set.from(_hiddenLibraryKeys)..remove(libraryKey); await _storageService.saveHiddenLibraries(_hiddenLibraryKeys); - notifyListeners(); + safeNotifyListeners(); } } @@ -63,6 +64,6 @@ class HiddenLibrariesProvider extends ChangeNotifier { /// Useful if storage was modified outside the provider Future refresh() async { _hiddenLibraryKeys = _storageService.getHiddenLibraries(); - notifyListeners(); + safeNotifyListeners(); } } diff --git a/lib/providers/libraries_provider.dart b/lib/providers/libraries_provider.dart index 340787d9..8af56c41 100644 --- a/lib/providers/libraries_provider.dart +++ b/lib/providers/libraries_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../models/plex_library.dart'; import '../services/data_aggregation_service.dart'; import '../services/storage_service.dart'; @@ -12,7 +13,7 @@ enum LibrariesLoadState { initial, loading, loaded, error } /// Provider that serves as the single source of truth for library data. /// Both SideNavigationRail and LibrariesScreen consume this provider /// instead of independently fetching library data. -class LibrariesProvider extends ChangeNotifier { +class LibrariesProvider extends ChangeNotifier with DisposableChangeNotifierMixin { DataAggregationService? _aggregationService; List _libraries = []; LibrariesLoadState _loadState = LibrariesLoadState.initial; @@ -60,7 +61,7 @@ class LibrariesProvider extends ChangeNotifier { _loadState = LibrariesLoadState.loading; _errorMessage = null; - notifyListeners(); + safeNotifyListeners(); try { // Fetch libraries from all servers @@ -79,12 +80,12 @@ class LibrariesProvider extends ChangeNotifier { _errorMessage = null; appLogger.i('LibrariesProvider: Loaded ${_libraries.length} libraries'); - notifyListeners(); + safeNotifyListeners(); } catch (e, stackTrace) { appLogger.e('LibrariesProvider: Failed to load libraries', error: e, stackTrace: stackTrace); _loadState = LibrariesLoadState.error; _errorMessage = e.toString(); - notifyListeners(); + safeNotifyListeners(); } } @@ -100,7 +101,7 @@ class LibrariesProvider extends ChangeNotifier { /// Update the library order and persist it. Future updateLibraryOrder(List orderedLibraries) async { _libraries = List.from(orderedLibraries); - notifyListeners(); + safeNotifyListeners(); // Save the new order final storage = await StorageService.getInstance(); @@ -115,7 +116,7 @@ class LibrariesProvider extends ChangeNotifier { _libraries = []; _loadState = LibrariesLoadState.initial; _errorMessage = null; - notifyListeners(); + safeNotifyListeners(); appLogger.d('LibrariesProvider: Cleared library data'); } diff --git a/lib/providers/offline_watch_provider.dart b/lib/providers/offline_watch_provider.dart index 26e0b51d..0f1c56b7 100644 --- a/lib/providers/offline_watch_provider.dart +++ b/lib/providers/offline_watch_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import '../i18n/strings.g.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../models/download_models.dart'; import '../models/plex_metadata.dart'; import '../services/offline_watch_sync_service.dart'; @@ -18,7 +19,7 @@ import '../utils/global_key_utils.dart'; /// - Effective watch status (local changes + cached server data) /// - Offline "OnDeck" calculation for shows /// - Manual mark watched/unwatched while offline -class OfflineWatchProvider extends ChangeNotifier { +class OfflineWatchProvider extends ChangeNotifier with DisposableChangeNotifierMixin { final OfflineWatchSyncService _syncService; final DownloadProvider _downloadProvider; @@ -30,7 +31,7 @@ class OfflineWatchProvider extends ChangeNotifier { } void _onSyncServiceChanged() { - notifyListeners(); + safeNotifyListeners(); } /// Whether a sync is in progress @@ -178,7 +179,7 @@ class OfflineWatchProvider extends ChangeNotifier { isNowWatched: true, changeType: WatchStateChangeType.watched, ); - notifyListeners(); + safeNotifyListeners(); _autoDeleteIfWatched(serverId, ratingKey); } @@ -219,7 +220,7 @@ class OfflineWatchProvider extends ChangeNotifier { isNowWatched: false, changeType: WatchStateChangeType.unwatched, ); - notifyListeners(); + safeNotifyListeners(); } /// Get downloaded episodes for a show with their watch status. diff --git a/lib/providers/playback_state_provider.dart b/lib/providers/playback_state_provider.dart index 531741c9..17245bb1 100644 --- a/lib/providers/playback_state_provider.dart +++ b/lib/providers/playback_state_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import '../models/plex_metadata.dart'; import '../models/play_queue_response.dart'; import '../services/plex_client.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; /// Result of trying to locate the current queue index. class _IndexLookupResult { @@ -14,7 +15,7 @@ class _IndexLookupResult { /// Manages playback state using Plex's play queue API. /// This provider is session-only and does not persist across app restarts. -class PlaybackStateProvider with ChangeNotifier { +class PlaybackStateProvider with ChangeNotifier, DisposableChangeNotifierMixin { // Play queue state int? _playQueueId; int _playQueueTotalCount = 0; @@ -61,7 +62,7 @@ class PlaybackStateProvider with ChangeNotifier { void setCurrentItem(PlexMetadata metadata) { if (_isQueueMode && metadata.playQueueItemID != null) { _currentPlayQueueItemID = metadata.playQueueItemID; - notifyListeners(); + safeNotifyListeners(); } } @@ -79,7 +80,7 @@ class PlaybackStateProvider with ChangeNotifier { _contextKey = contextKey; _isQueueMode = true; - notifyListeners(); + safeNotifyListeners(); } /// Load more items from the play queue if needed @@ -106,7 +107,7 @@ class PlaybackStateProvider with ChangeNotifier { // Use size or items length as fallback if totalCount is null _playQueueTotalCount = response.playQueueTotalCount ?? response.size ?? response.items!.length; _playQueueShuffled = response.playQueueShuffled; - notifyListeners(); + safeNotifyListeners(); return true; } } catch (e) { @@ -249,6 +250,6 @@ class PlaybackStateProvider with ChangeNotifier { _loadedItems = []; _contextKey = null; _isQueueMode = false; - notifyListeners(); + safeNotifyListeners(); } } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index cc6e31ff..84ee6c94 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../models/transcode_quality_preset.dart'; import '../services/settings_service.dart'; -class SettingsProvider extends ChangeNotifier { +class SettingsProvider extends ChangeNotifier with DisposableChangeNotifierMixin { SettingsService? _settingsService; bool _isInitialized = false; Future? _initFuture; @@ -21,7 +22,7 @@ class SettingsProvider extends ChangeNotifier { if (_isInitialized) return; _settingsService = await SettingsService.getInstance(); _isInitialized = true; - notifyListeners(); + safeNotifyListeners(); } /// Re-read settings after an external mutation (import, reset). The provider @@ -30,7 +31,7 @@ class SettingsProvider extends ChangeNotifier { Future reload() async { _settingsService = await SettingsService.getInstance(); _isInitialized = true; - notifyListeners(); + safeNotifyListeners(); } T _read(Pref pref, T fallback) => _isInitialized ? _settingsService!.read(pref) : fallback; @@ -39,7 +40,7 @@ class SettingsProvider extends ChangeNotifier { if (!_isInitialized) await _initializeSettings(); if (_settingsService!.read(pref) == value) return; await _settingsService!.write(pref, value); - notifyListeners(); + safeNotifyListeners(); } int get libraryDensity => _read(SettingsService.libraryDensity, LibraryDensity.defaultValue); diff --git a/lib/providers/shader_provider.dart b/lib/providers/shader_provider.dart index c5db8677..798b8ed8 100644 --- a/lib/providers/shader_provider.dart +++ b/lib/providers/shader_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../models/shader_preset.dart'; import '../services/settings_service.dart'; import '../services/shader_asset_loader.dart'; @@ -7,7 +8,7 @@ import '../services/shader_asset_loader.dart'; /// Provider for managing shader preset state. /// /// Persists the selected shader preset so it is restored across sessions. -class ShaderProvider extends ChangeNotifier { +class ShaderProvider extends ChangeNotifier with DisposableChangeNotifierMixin { late SettingsService _settingsService; ShaderPreset _savedPreset = ShaderPreset.none; @@ -31,7 +32,7 @@ class ShaderProvider extends ChangeNotifier { _currentPreset = _savedPreset; _initialized = true; - notifyListeners(); + safeNotifyListeners(); } /// Whether the provider has finished initializing @@ -63,7 +64,7 @@ class ShaderProvider extends ChangeNotifier { _savedPreset = preset; _currentPreset = preset; await _settingsService.write(SettingsService.globalShaderPreset, preset.id); - notifyListeners(); + safeNotifyListeners(); } /// Update the current preset without persisting (e.g. toggling off temporarily) @@ -84,7 +85,7 @@ class ShaderProvider extends ChangeNotifier { _customPresets.add(preset); await _saveCustomPresets(); - notifyListeners(); + safeNotifyListeners(); return preset; } @@ -103,7 +104,7 @@ class ShaderProvider extends ChangeNotifier { await _settingsService.write(SettingsService.globalShaderPreset, ShaderPreset.none.id); } - notifyListeners(); + safeNotifyListeners(); } Future _saveCustomPresets() async { diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index 115b8397..e518cbfc 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -2,10 +2,11 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../services/settings_service.dart' as settings; import '../theme/mono_theme.dart'; -class ThemeProvider extends ChangeNotifier { +class ThemeProvider extends ChangeNotifier with DisposableChangeNotifierMixin { late settings.SettingsService _settingsService; settings.ThemeMode _themeMode = settings.ThemeMode.system; late Brightness _systemBrightness; @@ -19,7 +20,7 @@ class ThemeProvider extends ChangeNotifier { void _onBrightnessChanged() { _systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; if (_themeMode == settings.ThemeMode.system) { - notifyListeners(); + safeNotifyListeners(); } } @@ -35,7 +36,7 @@ class ThemeProvider extends ChangeNotifier { _settingsService = await settings.SettingsService.getInstance(); _themeMode = _settingsService.read(settings.SettingsService.themeMode); _updateSplashTheme(_themeMode); - notifyListeners(); + safeNotifyListeners(); } settings.ThemeMode get themeMode => _themeMode; @@ -81,7 +82,7 @@ class ThemeProvider extends ChangeNotifier { _themeMode = mode; await _settingsService.write(settings.SettingsService.themeMode, mode); _updateSplashTheme(mode); - notifyListeners(); + safeNotifyListeners(); } } @@ -92,7 +93,7 @@ class ThemeProvider extends ChangeNotifier { final mode = _settingsService.read(settings.SettingsService.themeMode); _themeMode = mode; _updateSplashTheme(mode); - notifyListeners(); + safeNotifyListeners(); } void _updateSplashTheme(settings.ThemeMode mode) { diff --git a/lib/providers/trackers_provider.dart b/lib/providers/trackers_provider.dart index a62e6426..153bbe8f 100644 --- a/lib/providers/trackers_provider.dart +++ b/lib/providers/trackers_provider.dart @@ -24,11 +24,12 @@ import '../services/trackers/tracker_connect_runner.dart'; import '../services/trackers/tracker_constants.dart'; import '../services/trackers/tracker_coordinator.dart'; import '../utils/app_logger.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; /// Owns the active MAL / AniList / Simkl sessions for the currently-selected /// Plex profile. Single rebind seam: [onActiveProfileChanged] loads all three /// sessions from their stores and pushes them to their trackers. -class TrackersProvider extends ChangeNotifier { +class TrackersProvider extends ChangeNotifier with DisposableChangeNotifierMixin { final MalAuthService _malAuth = MalAuthService(); final AnilistAuthService _anilistAuth = AnilistAuthService(); final SimklAuthService _simklAuth = SimklAuthService(); @@ -77,7 +78,7 @@ class TrackersProvider extends ChangeNotifier { _anilist = results[1] as AnilistSession?; _simkl = results[2] as SimklSession?; _rebindAll(); - notifyListeners(); + safeNotifyListeners(); } // ───── Connect / disconnect ───── @@ -159,7 +160,7 @@ class TrackersProvider extends ChangeNotifier { if (_connecting != null || alreadyConnected) return false; _connecting = service; _cancelCompleter = Completer(); - notifyListeners(); + safeNotifyListeners(); try { return await runConnectPipeline( logLabel: service.name, @@ -173,14 +174,14 @@ class TrackersProvider extends ChangeNotifier { if (c != null && !c.isCompleted) c.complete(); _cancelCompleter = null; _connecting = null; - notifyListeners(); + safeNotifyListeners(); } } Future _clearAndRebind(TrackerAccountStore store, void Function() clearAndRebind) async { await store.clear(_activeUserUuid); clearAndRebind(); - notifyListeners(); + safeNotifyListeners(); } Future _enrichMal(MalSession raw) async { @@ -240,7 +241,7 @@ class TrackersProvider extends ChangeNotifier { onSessionUpdated: (next) { _mal = next; malAccountStore.save(_activeUserUuid, next); - notifyListeners(); + safeNotifyListeners(); }, ); } @@ -263,7 +264,7 @@ class TrackersProvider extends ChangeNotifier { store.clear(_activeUserUuid); clearSession(); rebind(); - notifyListeners(); + safeNotifyListeners(); } @override diff --git a/lib/providers/trakt_account_provider.dart b/lib/providers/trakt_account_provider.dart index 57623ad7..80b0c678 100644 --- a/lib/providers/trakt_account_provider.dart +++ b/lib/providers/trakt_account_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../models/trackers/device_code.dart'; import '../services/trackers/tracker_connect_runner.dart'; import '../services/trakt/trakt_account_store.dart'; @@ -16,7 +17,7 @@ import '../utils/app_logger.dart'; /// /// Single rebind seam: `onActiveProfileChanged` loads the new profile's /// session and pushes it to both `TraktScrobbleService` and `TraktSyncService`. -class TraktAccountProvider extends ChangeNotifier { +class TraktAccountProvider extends ChangeNotifier with DisposableChangeNotifierMixin { final TraktAuthService _auth = TraktAuthService(); final _store = traktAccountStore; @@ -71,7 +72,7 @@ class TraktAccountProvider extends ChangeNotifier { if (c != null && !c.isCompleted) c.complete(); _cancelCompleter = null; _isConnecting = false; - notifyListeners(); + safeNotifyListeners(); } } @@ -107,7 +108,7 @@ class TraktAccountProvider extends ChangeNotifier { session, onSessionInvalidated: _handleSessionInvalidated, ); - notifyListeners(); + safeNotifyListeners(); } /// Called by [TraktClient] when refresh fails permanently. Clears local state diff --git a/lib/providers/user_profile_provider.dart b/lib/providers/user_profile_provider.dart index 87f6bf7d..88279e9c 100644 --- a/lib/providers/user_profile_provider.dart +++ b/lib/providers/user_profile_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../mixins/disposable_change_notifier_mixin.dart'; import '../utils/plex_http_exception.dart'; import '../models/plex_home.dart'; import '../models/plex_home_user.dart'; @@ -8,7 +9,7 @@ import '../services/storage_service.dart'; import '../utils/app_logger.dart'; import '../screens/profile/pin_entry_dialog.dart'; -class UserProfileProvider extends ChangeNotifier { +class UserProfileProvider extends ChangeNotifier with DisposableChangeNotifierMixin { PlexHome? _home; PlexHomeUser? _currentUser; PlexUserProfile? _profileSettings; @@ -117,7 +118,7 @@ class UserProfileProvider extends ChangeNotifier { // Profile settings are NOT cached - they will be fetched fresh from API // in refreshProfileSettings() - notifyListeners(); + safeNotifyListeners(); } /// Fetch the user's profile settings from the API @@ -140,7 +141,7 @@ class UserProfileProvider extends ChangeNotifier { appLogger.i('Successfully fetched user profile settings from API'); - notifyListeners(); + safeNotifyListeners(); } catch (e) { appLogger.w('Failed to fetch user profile settings from API', error: e); // Don't set error state, profile will remain null or keep existing value @@ -210,7 +211,7 @@ class UserProfileProvider extends ChangeNotifier { } } - notifyListeners(); + safeNotifyListeners(); } catch (e) { _setError('Failed to load home users: $e'); appLogger.e('Failed to load home users', error: e); @@ -286,7 +287,7 @@ class UserProfileProvider extends ChangeNotifier { }, ); - notifyListeners(); + safeNotifyListeners(); // Invalidate all cached data and reconnect to all servers with new tokens // The callback will handle server reconnection using the servers list @@ -337,7 +338,7 @@ class UserProfileProvider extends ChangeNotifier { // Update current user from refreshed data if (_home != null) { _currentUser = _home!.getUserByUUID(_currentUser!.uuid); - notifyListeners(); + safeNotifyListeners(); } } } @@ -361,7 +362,7 @@ class UserProfileProvider extends ChangeNotifier { _isInitialized = false; _clearError(); - notifyListeners(); + safeNotifyListeners(); appLogger.i('User logged out successfully'); } catch (e) { @@ -373,12 +374,12 @@ class UserProfileProvider extends ChangeNotifier { void _setLoading(bool loading) { _isLoading = loading; - notifyListeners(); + safeNotifyListeners(); } void _setError(String error) { _error = error; - notifyListeners(); + safeNotifyListeners(); } void _clearError() {