@tduncan / Cortana / commits / b070027

fix(providers): guard async Riverpod operations against disposed Ref access

Add ref.mounted checks after async gaps in notifiers and callbacks to prevent the 'Cannot use the Ref ... after it has been disposed' crash (issue #69). Covers ChatNotifier streaming/listener paths, ForegroundDownloadNotifier progress + delayed removal, ImportedGgufModelsNotifier imports/deletes, SavedMessageFoldersNotifier, ServersNotifier status/tests, CloudSyncController configure/sync/apply, and onboarding server setup. _saveMessage now self-guards so all 24 call sites are covered at once. Fixes #69

abdulmominsakib committed Aug 24, 2026 at 13:09 UTC b070027ce26a021b383a71da65b4f45e107969bc
7 files changed +273 -177
lib/features/chat/providers/chat_notifier.dart
+90 -52
@@ -924,7 +924,7 @@ class ChatNotifier extends Notifier<ChatState> {
924 bool streamHadError = false;
925
926 void updateUiState() {
927 - if (!stateNeedsUpdate) return;
927 + if (!stateNeedsUpdate || !ref.mounted) return;
928 stateNeedsUpdate = false;
929
930 final streamConvId = assistantMessage.conversationId;
@@ -951,7 +951,9 @@ class ChatNotifier extends Notifier<ChatState> {
951 )
952 .listen(
953 (response) async {
954 - if (streamGeneration != _streamGeneration) return;
954 + if (streamGeneration != _streamGeneration || !ref.mounted) {
955 + return;
956 + }
957 final streamConvId = assistantMessage.conversationId;
958 final isCurrentContext = _currentConversationId == streamConvId;
959
@@ -1070,14 +1072,18 @@ class ChatNotifier extends Notifier<ChatState> {
1072 if (!_isInMemoryChat) {
1073 await _saveService?.flush();
1074 }
1073 - ref.read(chatBackgroundServiceProvider).stop();
1075 + if (ref.mounted) {
1076 + ref.read(chatBackgroundServiceProvider).stop();
1077 + }
1078 return;
1079 }
1080
1081 if (!_isInMemoryChat) {
1082 await _saveService?.flush();
1083 }
1080 - ref.read(chatBackgroundServiceProvider).stop();
1084 + if (ref.mounted) {
1085 + ref.read(chatBackgroundServiceProvider).stop();
1086 + }
1087 final streamConvId = assistantMessage.conversationId;
1088 final isCurrentContext = _activeConversationId == streamConvId;
1089 final streamingMessage = streamingAssistantMessage;
@@ -1094,14 +1100,16 @@ class ChatNotifier extends Notifier<ChatState> {
1100 isProcessing: false,
1101 );
1102 await _saveMessage(errorMessage);
1097 - if (isCurrentContext) {
1103 + if (ref.mounted && isCurrentContext) {
1104 _replaceMessageInState(errorMessage, clearStreaming: true);
1105 state = state.copyWith(
1106 isStreaming: false,
1107 errorMessage: errorMessage.errorMessage,
1108 );
1109 }
1104 - ref.read(isStreamingProvider.notifier).setStreaming(false);
1110 + if (ref.mounted) {
1111 + ref.read(isStreamingProvider.notifier).setStreaming(false);
1112 + }
1113 _latestStreamingMessage = null;
1114 } else {
1115 var finalMessage = _finalizeStreamMessage(
@@ -1156,7 +1164,7 @@ class ChatNotifier extends Notifier<ChatState> {
1164 }
1165 }
1166
1159 - if (clientExecuted.isNotEmpty) {
1167 + if (clientExecuted.isNotEmpty && ref.mounted) {
1168 final registry = ref.read(toolRegistryProvider);
1169 final adapter = createAdapterForServerType(server.type);
1170
@@ -1180,14 +1188,20 @@ class ChatNotifier extends Notifier<ChatState> {
1188 completer: completer,
1189 );
1190 _pendingToolApproval = approval;
1183 - state = state.copyWith(
1184 - pendingToolApproval: approval,
1185 - );
1191 + if (ref.mounted) {
1192 + state = state.copyWith(
1193 + pendingToolApproval: approval,
1194 + );
1195 + }
1196
1197 final result = await completer.future;
1198
1199 _pendingToolApproval = null;
1190 - state = state.copyWith(clearPendingApproval: true);
1200 + if (ref.mounted) {
1201 + state = state.copyWith(
1202 + clearPendingApproval: true,
1203 + );
1204 + }
1205
1206 return result;
1207 },
@@ -1215,12 +1229,14 @@ class ChatNotifier extends Notifier<ChatState> {
1229 }
1230
1231 await _saveMessage(finalMessage);
1218 - if (isCurrentContext) {
1232 + if (ref.mounted && isCurrentContext) {
1233 _replaceMessageInState(finalMessage, clearStreaming: true);
1234 state = state.copyWith(isStreaming: false);
1235 _maybeAutoGenerateTitleAfterFirstReply();
1236 }
1223 - ref.read(isStreamingProvider.notifier).setStreaming(false);
1237 + if (ref.mounted) {
1238 + ref.read(isStreamingProvider.notifier).setStreaming(false);
1239 + }
1240 _latestStreamingMessage = null;
1241
1242 await _syncConversationStatsAfterGeneration(
@@ -1229,11 +1245,13 @@ class ChatNotifier extends Notifier<ChatState> {
1245 isCurrentContext: isCurrentContext,
1246 );
1247
1232 - _maybeRequestReviewAfterSuccessfulCompletion(
1233 - finalMessage: finalMessage,
1234 - server: server,
1235 - selectedModel: selectedModel,
1236 - );
1248 + if (ref.mounted) {
1249 + _maybeRequestReviewAfterSuccessfulCompletion(
1250 + finalMessage: finalMessage,
1251 + server: server,
1252 + selectedModel: selectedModel,
1253 + );
1254 + }
1255
1256 _chunkCount = 0;
1257 _lastCheckpointTime = null;
@@ -1242,19 +1260,24 @@ class ChatNotifier extends Notifier<ChatState> {
1260
1261 // Auto-speak: read the response aloud if the setting
1262 // is enabled and voice mode is not already handling TTS.
1245 - final autoSpeak = ref.read(settingsProvider).autoSpeakEnabled;
1246 - final voiceActive =
1247 - ref.read(voiceModeProvider).phase != VoiceModePhase.idle;
1248 - if (autoSpeak &&
1249 - !voiceActive &&
1250 - finalMessage.content.trim().isNotEmpty) {
1251 - ref
1252 - .read(ttsProvider.notifier)
1253 - .speak(
1254 - finalMessage.content,
1255 - messageId: finalMessage.id,
1256 - conversationId: finalMessage.conversationId,
1257 - );
1263 + if (ref.mounted) {
1264 + final autoSpeak = ref
1265 + .read(settingsProvider)
1266 + .autoSpeakEnabled;
1267 + final voiceActive =
1268 + ref.read(voiceModeProvider).phase !=
1269 + VoiceModePhase.idle;
1270 + if (autoSpeak &&
1271 + !voiceActive &&
1272 + finalMessage.content.trim().isNotEmpty) {
1273 + ref
1274 + .read(ttsProvider.notifier)
1275 + .speak(
1276 + finalMessage.content,
1277 + messageId: finalMessage.id,
1278 + conversationId: finalMessage.conversationId,
1279 + );
1280 + }
1281 }
1282 }
1283 },
@@ -1275,10 +1298,12 @@ class ChatNotifier extends Notifier<ChatState> {
1298 if (errorMessage != null) {
1299 await _saveMessage(errorMessage);
1300
1278 - ref.read(chatBackgroundServiceProvider).stop();
1301 + if (ref.mounted) {
1302 + ref.read(chatBackgroundServiceProvider).stop();
1303 + }
1304
1305 final streamConvId = assistantMessage.conversationId;
1281 - if (_currentConversationId == streamConvId) {
1306 + if (ref.mounted && _currentConversationId == streamConvId) {
1307 _replaceMessageInState(errorMessage, clearStreaming: true);
1308 state = state.copyWith(
1309 isStreaming: false,
@@ -1286,7 +1311,9 @@ class ChatNotifier extends Notifier<ChatState> {
1311 );
1312 }
1313 }
1289 - ref.read(isStreamingProvider.notifier).setStreaming(false);
1314 + if (ref.mounted) {
1315 + ref.read(isStreamingProvider.notifier).setStreaming(false);
1316 + }
1317 _latestStreamingMessage = null;
1318 },
1319 );
@@ -1301,6 +1328,8 @@ class ChatNotifier extends Notifier<ChatState> {
1328 );
1329 await _saveMessage(errorMsg);
1330
1331 + if (!ref.mounted) return;
1332 +
1333 state = state.copyWith(
1334 isStreaming: false,
1335 errorMessage: e.toString(),
@@ -1390,6 +1419,8 @@ class ChatNotifier extends Notifier<ChatState> {
1419
1420 await _saveMessage(message);
1421
1422 + if (!ref.mounted) return;
1423 +
1424 if (!_isInMemoryChat && _currentConversationId != null) {
1425 final preview = trimmedContent.isNotEmpty
1426 ? (trimmedContent.length > 100
@@ -2184,8 +2215,10 @@ class ChatNotifier extends Notifier<ChatState> {
2215 );
2216 _maybeAutoGenerateTitleAfterFirstReply();
2217 }
2187 - ref.read(isStreamingProvider.notifier).setStreaming(false);
2188 - ref.read(chatBackgroundServiceProvider).stop();
2218 + if (ref.mounted) {
2219 + ref.read(isStreamingProvider.notifier).setStreaming(false);
2220 + ref.read(chatBackgroundServiceProvider).stop();
2221 + }
2222 await _syncConversationStatsAfterGeneration(
2223 streamConvId,
2224 finalMessage,
@@ -2200,19 +2233,21 @@ class ChatNotifier extends Notifier<ChatState> {
2233
2234 // Auto-speak: read the response aloud if the setting is
2235 // enabled and voice mode is not already handling TTS.
2203 - final autoSpeak = ref.read(settingsProvider).autoSpeakEnabled;
2204 - final voiceActive =
2205 - ref.read(voiceModeProvider).phase != VoiceModePhase.idle;
2206 - if (autoSpeak &&
2207 - !voiceActive &&
2208 - finalMessage.content.trim().isNotEmpty) {
2209 - ref
2210 - .read(ttsProvider.notifier)
2211 - .speak(
2212 - finalMessage.content,
2213 - messageId: finalMessage.id,
2214 - conversationId: finalMessage.conversationId,
2215 - );
2236 + if (ref.mounted) {
2237 + final autoSpeak = ref.read(settingsProvider).autoSpeakEnabled;
2238 + final voiceActive =
2239 + ref.read(voiceModeProvider).phase != VoiceModePhase.idle;
2240 + if (autoSpeak &&
2241 + !voiceActive &&
2242 + finalMessage.content.trim().isNotEmpty) {
2243 + ref
2244 + .read(ttsProvider.notifier)
2245 + .speak(
2246 + finalMessage.content,
2247 + messageId: finalMessage.id,
2248 + conversationId: finalMessage.conversationId,
2249 + );
2250 + }
2251 }
2252 },
2253 onError: (Object error, StackTrace stackTrace) async {
@@ -2236,8 +2271,10 @@ class ChatNotifier extends Notifier<ChatState> {
2271 errorMessage: error.toString(),
2272 );
2273 }
2239 - ref.read(isStreamingProvider.notifier).setStreaming(false);
2240 - ref.read(chatBackgroundServiceProvider).stop();
2274 + if (ref.mounted) {
2275 + ref.read(isStreamingProvider.notifier).setStreaming(false);
2276 + ref.read(chatBackgroundServiceProvider).stop();
2277 + }
2278 },
2279 );
2280 } catch (e) {
@@ -2530,7 +2567,7 @@ class ChatNotifier extends Notifier<ChatState> {
2567 }
2568
2569 Future<void> _saveMessage(Message message) async {
2533 - if (_isInMemoryChat) return;
2570 + if (_isInMemoryChat || !ref.mounted) return;
2571 final db = ref.read(databaseProvider);
2572 await db.store.runInTransactionAsync(
2573 TxMode.write,
@@ -2570,6 +2607,7 @@ class ChatNotifier extends Notifier<ChatState> {
2607
2608 Future<void> clearConversation() async {
2609 await _saveService?.flush();
2610 + if (!ref.mounted) return;
2611 final db = ref.read(databaseProvider);
2612 if (_currentConversationId != null && !_isInMemoryChat) {
2613 final query = db.messageBox
lib/features/cloud_sync/providers/cloud_sync_providers.dart
+19 -4
@@ -121,16 +121,19 @@ class CloudSyncController extends Notifier<CloudSyncStatus> {
121 masterKey: masterKey,
122 salt: salt,
123 );
124 + if (!ref.mounted) return;
125 state = state.copyWith(
126 phase: CloudSyncPhase.ready,
127 message: 'Cloud sync enabled.',
128 );
129 await syncNow(rethrowErrors: true);
130 } catch (error) {
130 - state = state.copyWith(
131 - phase: CloudSyncPhase.error,
132 - message: error.toString(),
133 - );
131 + if (ref.mounted) {
132 + state = state.copyWith(
133 + phase: CloudSyncPhase.error,
134 + message: error.toString(),
135 + );
136 + }
137 rethrow;
138 }
139 }
@@ -234,6 +237,12 @@ class CloudSyncController extends Notifier<CloudSyncStatus> {
237 credentials: credentials,
238 );
239 final journal = await _local.loadJournal();
240 + if (!ref.mounted) {
241 + throw const CloudSyncFailure(
242 + CloudSyncFailureKind.cancelled,
243 + 'Cloud sync was cancelled.',
244 + );
245 + }
246 final payload = DataBackupService().exportCloudSync(
247 ref.read(databaseProvider).store,
248 ref.read(settingsProvider).toJson(),
@@ -521,6 +530,12 @@ class CloudSyncController extends Notifier<CloudSyncStatus> {
530 ) async {
531 final payload = Map<String, dynamic>.from(snapshot.payload);
532 await _materializeAttachments(payload, repository, masterKey);
533 + if (!ref.mounted) {
534 + throw const CloudSyncFailure(
535 + CloudSyncFailureKind.cancelled,
536 + 'Cloud sync was cancelled.',
537 + );
538 + }
539 _ensureActive(generation);
540 AppSettings? importedSettings;
541 final remoteSettings = payload['settings'];
lib/features/on_device/providers/foreground_download_providers.dart
+28 -27
@@ -101,7 +101,7 @@ class ForegroundDownloadNotifier
101 model,
102 token: (hfToken != null && hfToken.isNotEmpty) ? hfToken : null,
103 onProgress: (progress) {
104 - if (_activeDownloads[modelId] != true) return;
104 + if (!ref.mounted || _activeDownloads[modelId] != true) return;
105
106 final startTime = _downloadStartTimes[modelId] ?? DateTime.now();
107 final elapsed = DateTime.now().difference(startTime).inSeconds;
@@ -131,31 +131,33 @@ class ForegroundDownloadNotifier
131
132 // Download completed
133 if (_activeDownloads[modelId] == true) {
134 - state = {
135 - ...state,
136 - modelId: DownloadProgressInfo(
137 - modelId: modelId,
138 - status: DownloadStatus.complete,
139 - progress: 1.0,
140 - receivedBytes: model.fileSizeBytes,
141 - totalBytes: model.fileSizeBytes,
142 - ),
143 - };
144 - _saveState();
134 + if (ref.mounted) {
135 + state = {
136 + ...state,
137 + modelId: DownloadProgressInfo(
138 + modelId: modelId,
139 + status: DownloadStatus.complete,
140 + progress: 1.0,
141 + receivedBytes: model.fileSizeBytes,
142 + totalBytes: model.fileSizeBytes,
143 + ),
144 + };
145 + _saveState();
146
146 - ref.invalidate(downloadedModelsProvider);
147 - try {
148 - await ref
149 - .read(reviewPromptServiceProvider)
150 - .markModelDownloadCompleted(modelId);
151 - } catch (e) {
152 - Log.error('Failed to record model download review signal: $e');
147 + ref.invalidate(downloadedModelsProvider);
148 + try {
149 + await ref
150 + .read(reviewPromptServiceProvider)
151 + .markModelDownloadCompleted(modelId);
152 + } catch (e) {
153 + Log.error('Failed to record model download review signal: $e');
154 + }
155 + _removeCompletedDownload(modelId);
156 }
154 - _removeCompletedDownload(modelId);
157 }
158 } catch (e) {
159 Log.error('Download failed for $modelId: $e');
158 - if (_activeDownloads[modelId] == true) {
160 + if (_activeDownloads[modelId] == true && ref.mounted) {
161 state = {
162 ...state,
163 modelId: DownloadProgressInfo(
@@ -176,12 +178,11 @@ class ForegroundDownloadNotifier
178
179 void _removeCompletedDownload(String modelId) {
180 Future.delayed(const Duration(seconds: 1), () {
179 - if (state.containsKey(modelId)) {
180 - final current = Map<String, DownloadProgressInfo>.from(state);
181 - current.remove(modelId);
182 - state = current;
183 - _saveState();
184 - }
181 + if (!ref.mounted || !state.containsKey(modelId)) return;
182 + final current = Map<String, DownloadProgressInfo>.from(state);
183 + current.remove(modelId);
184 + state = current;
185 + _saveState();
186 });
187 }
188
lib/features/on_device/providers/on_device_providers.dart
+22 -10
@@ -135,7 +135,9 @@ class ImportedGgufModelsNotifier extends Notifier<List<OnDeviceModel>> {
135 Future<OnDeviceModel> importModel(String sourcePath) async {
136 final metadata = await _repository.importFromPath(sourcePath);
137 final model = metadata.toOnDeviceModel();
138 - state = [...state, model];
138 + if (ref.mounted) {
139 + state = [...state, model];
140 + }
141 return model;
142 }
143
@@ -152,12 +154,15 @@ class ImportedGgufModelsNotifier extends Notifier<List<OnDeviceModel>> {
154 cancelToken: cancelToken,
155 );
156 final model = metadata.toOnDeviceModel();
155 - state = [...state, model];
157 + if (ref.mounted) {
158 + state = [...state, model];
159 + }
160 return model;
161 }
162
163 Future<void> deleteModel(String modelId) async {
164 await _repository.delete(modelId);
165 + if (!ref.mounted) return;
166 state = state.where((model) => model.id != modelId).toList();
167 }
168
@@ -168,6 +173,7 @@ class ImportedGgufModelsNotifier extends Notifier<List<OnDeviceModel>> {
173 final hadModel = state.any((m) => m.id == modelId);
174 if (!hadModel) return;
175 await _repository.delete(modelId);
176 + if (!ref.mounted) return;
177 state = state.where((model) => model.id != modelId).toList();
178 }
179
@@ -356,15 +362,21 @@ class OnDeviceEngineNotifier extends Notifier<OnDeviceEngineState> {
362 }
363
364 Future<void> unloadModel() async {
359 - try {
360 - await _gemmaService.unloadModel();
361 - } catch (e) {
362 - Log.error('Failed to unload gemma model: $e');
365 + final gemma = ref.mounted ? _gemmaService : null;
366 + final llama = ref.mounted ? _llamaService : null;
367 + if (gemma != null) {
368 + try {
369 + await gemma.unloadModel();
370 + } catch (e) {
371 + Log.error('Failed to unload gemma model: $e');
372 + }
373 }
364 - try {
365 - await _llamaService.unloadModel();
366 - } catch (e) {
367 - Log.error('Failed to unload llama.cpp model: $e');
374 + if (llama != null) {
375 + try {
376 + await llama.unloadModel();
377 + } catch (e) {
378 + Log.error('Failed to unload llama.cpp model: $e');
379 + }
380 }
381 if (ref.mounted) {
382 state = const OnDeviceEngineState();
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
+25 -22
@@ -93,8 +93,7 @@ class _OnboardingServerSetupScreenState
93 }
94
95 Server _buildServer() {
96 - final requiresCloudConfig =
97 - widget.selectedType == ServerType.ollamaCloud;
96 + final requiresCloudConfig = widget.selectedType == ServerType.ollamaCloud;
97 final host = requiresCloudConfig
98 ? AppConstants.ollamaCloudBaseUrl
99 : _hostController.text.trim();
@@ -217,7 +216,9 @@ class _OnboardingServerSetupScreenState
216 try {
217 final server = _buildServer();
218 await ref.read(serversProvider.notifier).addServer(server);
219 + if (!mounted) return;
220 await ref.read(serversProvider.notifier).setDefault(server.id);
221 + if (!mounted) return;
222 invalidateAvailableModelsCache(server.id);
223 ref.read(activeServerIdProvider.notifier).setActiveServer(server);
224
@@ -266,10 +267,7 @@ class _OnboardingServerSetupScreenState
267 ),
268 children: [
269 Actor(
269 - acts: [
270 - .fadeIn(),
271 - .slideY(from: 0.08),
272 - ],
270 + acts: [.fadeIn(), .slideY(from: 0.08)],
271 child: Text(
272 l10n.setup_connection_desc(widget.selectedType.name),
273 style: theme.textTheme.titleMedium?.copyWith(
@@ -279,10 +277,7 @@ class _OnboardingServerSetupScreenState
277 ),
278 Actor(
279 delay: 60.ms,
282 - acts: [
283 - .fadeIn(),
284 - .slideY(from: 0.08),
285 - ],
280 + acts: [.fadeIn(), .slideY(from: 0.08)],
281 child: Column(
282 crossAxisAlignment: CrossAxisAlignment.stretch,
283 children: [
@@ -357,10 +352,14 @@ class _OnboardingServerSetupScreenState
352 Container(
353 padding: const EdgeInsets.all(12),
354 decoration: BoxDecoration(
360 - color: theme.colorScheme.primary.withValues(alpha: 0.08),
355 + color: theme.colorScheme.primary.withValues(
356 + alpha: 0.08,
357 + ),
358 borderRadius: BorderRadius.circular(8),
359 border: Border.all(
363 - color: theme.colorScheme.primary.withValues(alpha: 0.2),
360 + color: theme.colorScheme.primary.withValues(
361 + alpha: 0.2,
362 + ),
363 ),
364 ),
365 child: Row(
@@ -378,7 +377,8 @@ class _OnboardingServerSetupScreenState
377 ? l10n.ollama_cloud_disclosure
378 : l10n.openrouter_disclosure,
379 style: theme.textTheme.bodySmall?.copyWith(
381 - color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
380 + color: theme.colorScheme.onSurface
381 + .withValues(alpha: 0.8),
382 height: 1.4,
383 ),
384 ),
@@ -392,10 +392,7 @@ class _OnboardingServerSetupScreenState
392 ),
393 Actor(
394 delay: 120.ms,
395 - acts: [
396 - .fadeIn(),
397 - .slideY(from: 0.08),
398 - ],
395 + acts: [.fadeIn(), .slideY(from: 0.08)],
396 child: Column(
397 crossAxisAlignment: CrossAxisAlignment.stretch,
398 children: [
@@ -415,8 +412,10 @@ class _OnboardingServerSetupScreenState
412 ),
413 child: Row(
414 children: [
418 - HugeIcon(icon:
419 - _testSuccess ? HugeIcons.strokeRoundedCheckmarkCircle01 : HugeIcons.strokeRoundedAlertCircle,
415 + HugeIcon(
416 + icon: _testSuccess
417 + ? HugeIcons.strokeRoundedCheckmarkCircle01
418 + : HugeIcons.strokeRoundedAlertCircle,
419 color: _testSuccess ? Colors.green : Colors.red,
420 ),
421 const SizedBox(width: 8),
@@ -424,7 +423,9 @@ class _OnboardingServerSetupScreenState
423 child: Text(
424 _testResult!,
425 style: TextStyle(
427 - color: _testSuccess ? Colors.green : Colors.red,
426 + color: _testSuccess
427 + ? Colors.green
428 + : Colors.red,
429 ),
430 ),
431 ),
@@ -438,7 +439,9 @@ class _OnboardingServerSetupScreenState
439 ? const SizedBox(
440 width: 20,
441 height: 20,
441 - child: CircularProgressIndicator(strokeWidth: 2),
442 + child: CircularProgressIndicator(
443 + strokeWidth: 2,
444 + ),
445 )
446 : Text(l10n.test_connection),
447 ),
@@ -473,4 +476,4 @@ class _OnboardingServerSetupScreenState
476 ),
477 );
478 }
476 -}
\ No newline at end of file
479 +}
lib/features/saved_messages/providers/saved_message_providers.dart
+56 -52
@@ -16,9 +16,11 @@ final savedMessageFolderFilterProvider =
16 enum SavedMessageListFilter { all, tempChats, user, assistant, archived }
17
18 final savedMessageListFilterProvider =
19 - NotifierProvider<SavedMessageListFilterNotifier, SavedMessageListFilter>(() {
20 - return SavedMessageListFilterNotifier();
21 - });
19 + NotifierProvider<SavedMessageListFilterNotifier, SavedMessageListFilter>(
20 + () {
21 + return SavedMessageListFilterNotifier();
22 + },
23 + );
24
25 class SavedMessageListFilterNotifier extends Notifier<SavedMessageListFilter> {
26 @override
@@ -70,9 +72,10 @@ class SavedMessageSelectedIdsNotifier extends Notifier<Set<String>> {
72 }
73
74 final savedMessageFoldersProvider =
73 - AsyncNotifierProvider<SavedMessageFoldersNotifier, List<SavedMessageFolder>>(
74 - () => SavedMessageFoldersNotifier(),
75 - );
75 + AsyncNotifierProvider<
76 + SavedMessageFoldersNotifier,
77 + List<SavedMessageFolder>
78 + >(() => SavedMessageFoldersNotifier());
79
80 class SavedMessageFoldersNotifier
81 extends AsyncNotifier<List<SavedMessageFolder>> {
@@ -112,6 +115,7 @@ class SavedMessageFoldersNotifier
115 createdAt: folder.createdAt,
116 ),
117 );
118 + if (!ref.mounted) return folder;
119 state = AsyncData(await _loadAll());
120 return folder;
121 }
@@ -126,6 +130,7 @@ class SavedMessageFoldersNotifier
130 if (entity == null) return;
131 entity.name = name;
132 db.savedMessageFolderBox.put(entity);
133 + if (!ref.mounted) return;
134 state = AsyncData(await _loadAll());
135 }
136
@@ -146,6 +151,7 @@ class SavedMessageFoldersNotifier
151 }
152 msgQuery.close();
153
154 + if (!ref.mounted) return;
155 ref.invalidate(savedMessagesProvider);
156 state = AsyncData(await _loadAll());
157 }
@@ -247,9 +253,7 @@ class SavedMessagesNotifier extends AsyncNotifier<List<SavedMessage>> {
253 id: DateTime.now().millisecondsSinceEpoch.toString(),
254 sourceMessageId: message.id,
255 conversationId: isTemporaryChat ? '' : message.conversationId,
250 - conversationTitle: isTemporaryChat
251 - ? ''
252 - : (conversation?.title ?? 'Chat'),
256 + conversationTitle: isTemporaryChat ? '' : (conversation?.title ?? 'Chat'),
257 roleIndex: message.role.index,
258 content: message.content,
259 modelId: message.modelId,
@@ -326,49 +330,49 @@ class SavedMessagesNotifier extends AsyncNotifier<List<SavedMessage>> {
330 }
331 }
332
329 -final filteredSavedMessagesProvider =
330 - Provider<AsyncValue<List<SavedMessage>>>((ref) {
331 - final messagesAsync = ref.watch(savedMessagesProvider);
332 - final folderFilter = ref.watch(savedMessageFolderFilterProvider);
333 - final listFilter = ref.watch(savedMessageListFilterProvider);
334 -
335 - return messagesAsync.whenData((messages) {
336 - var filtered = messages;
337 -
338 - switch (listFilter) {
339 - case SavedMessageListFilter.archived:
340 - filtered = filtered.where((m) => m.isArchived).toList();
341 - case SavedMessageListFilter.tempChats:
342 - filtered = filtered
343 - .where((m) => !m.isArchived && m.conversationId.isEmpty)
344 - .toList();
345 - case SavedMessageListFilter.user:
346 - filtered = filtered
347 - .where(
348 - (m) => !m.isArchived && m.roleIndex == MessageRole.user.index,
349 - )
350 - .toList();
351 - case SavedMessageListFilter.assistant:
352 - filtered = filtered
353 - .where(
354 - (m) =>
355 - !m.isArchived &&
356 - m.roleIndex == MessageRole.assistant.index,
357 - )
358 - .toList();
359 - case SavedMessageListFilter.all:
360 - filtered = filtered.where((m) => !m.isArchived).toList();
361 - }
362 -
363 - if (folderFilter == null) return filtered;
364 - if (folderFilter.isEmpty) {
365 - return filtered
366 - .where((m) => m.folderId == null || m.folderId!.isEmpty)
367 - .toList();
368 - }
369 - return filtered.where((m) => m.folderId == folderFilter).toList();
370 - });
371 - });
333 +final filteredSavedMessagesProvider = Provider<AsyncValue<List<SavedMessage>>>((
334 + ref,
335 +) {
336 + final messagesAsync = ref.watch(savedMessagesProvider);
337 + final folderFilter = ref.watch(savedMessageFolderFilterProvider);
338 + final listFilter = ref.watch(savedMessageListFilterProvider);
339 +
340 + return messagesAsync.whenData((messages) {
341 + var filtered = messages;
342 +
343 + switch (listFilter) {
344 + case SavedMessageListFilter.archived:
345 + filtered = filtered.where((m) => m.isArchived).toList();
346 + case SavedMessageListFilter.tempChats:
347 + filtered = filtered
348 + .where((m) => !m.isArchived && m.conversationId.isEmpty)
349 + .toList();
350 + case SavedMessageListFilter.user:
351 + filtered = filtered
352 + .where(
353 + (m) => !m.isArchived && m.roleIndex == MessageRole.user.index,
354 + )
355 + .toList();
356 + case SavedMessageListFilter.assistant:
357 + filtered = filtered
358 + .where(
359 + (m) =>
360 + !m.isArchived && m.roleIndex == MessageRole.assistant.index,
361 + )
362 + .toList();
363 + case SavedMessageListFilter.all:
364 + filtered = filtered.where((m) => !m.isArchived).toList();
365 + }
366 +
367 + if (folderFilter == null) return filtered;
368 + if (folderFilter.isEmpty) {
369 + return filtered
370 + .where((m) => m.folderId == null || m.folderId!.isEmpty)
371 + .toList();
372 + }
373 + return filtered.where((m) => m.folderId == folderFilter).toList();
374 + });
375 +});
376
377 final isMessageSavedProvider = FutureProvider.family<bool, String>((
378 ref,
lib/features/servers/providers/server_providers.dart
+33 -10
@@ -303,6 +303,7 @@ class ServersNotifier extends AsyncNotifier<List<Server>> {
303 db.serverBox.put(entity);
304 invalidateAvailableModelsCache(updatedServer.id);
305
306 + if (!ref.mounted) return;
307 state = AsyncData(await _loadAll());
308 }
309
@@ -321,6 +322,11 @@ class ServersNotifier extends AsyncNotifier<List<Server>> {
322 }
323
324 final isConnected = await apiService.testConnection(server);
325 + if (!ref.mounted) {
326 + return isConnected
327 + ? ConnectionStatus.connected
328 + : ConnectionStatus.disconnected;
329 + }
330 final status = isConnected
331 ? ConnectionStatus.connected
332 : ConnectionStatus.disconnected;
@@ -332,13 +338,15 @@ class ServersNotifier extends AsyncNotifier<List<Server>> {
338 Future<void> testAllConnections(dynamic apiService) async {
339 final servers = state.value ?? [];
340 for (final server in servers) {
341 + if (!ref.mounted) return;
342 await testConnection(server.id, apiService);
343 }
344 }
345 }
346
340 -
347 class ConnectionStatusNotifier extends Notifier<ConnectionStatus> {
348 + int _checkGeneration = 0;
349 +
350 @override
351 ConnectionStatus build() {
352 final activeServer = ref.watch(activeServerProvider);
@@ -352,34 +360,49 @@ class ConnectionStatusNotifier extends Notifier<ConnectionStatus> {
360 return ConnectionStatus.connected;
361 }
362
355 - _checkConnection(activeServer, apiService);
363 + final generation = ++_checkGeneration;
364 + Future.microtask(
365 + () => _checkConnection(activeServer, apiService, generation),
366 + );
367 return ConnectionStatus.checking;
368 }
369
359 - Future<void> _checkConnection(Server server, dynamic apiService) async {
370 + Future<void> _checkConnection(
371 + Server server,
372 + dynamic apiService,
373 + int generation,
374 + ) async {
375 try {
376 final isConnected = await apiService.testConnection(server);
377 + if (!ref.mounted || generation != _checkGeneration) return;
378 final status = isConnected
379 ? ConnectionStatus.connected
380 : ConnectionStatus.disconnected;
381 state = status;
366 - ref
367 - .read(serversProvider.notifier)
368 - .updateServerStatus(server.id, status);
382 + if (ref.mounted) {
383 + ref
384 + .read(serversProvider.notifier)
385 + .updateServerStatus(server.id, status);
386 + }
387 } catch (e) {
388 + if (!ref.mounted || generation != _checkGeneration) return;
389 state = ConnectionStatus.error;
371 - ref
372 - .read(serversProvider.notifier)
373 - .updateServerStatus(server.id, ConnectionStatus.disconnected);
390 + if (ref.mounted) {
391 + ref
392 + .read(serversProvider.notifier)
393 + .updateServerStatus(server.id, ConnectionStatus.disconnected);
394 + }
395 }
396 }
397
398 Future<void> refresh() async {
399 + if (!ref.mounted) return;
400 final activeServer = ref.read(activeServerProvider);
401 final apiService = ref.read(serverApiServiceProvider);
402 if (activeServer != null) {
403 state = ConnectionStatus.checking;
382 - await _checkConnection(activeServer, apiService);
404 + final generation = ++_checkGeneration;
405 + await _checkConnection(activeServer, apiService, generation);
406 }
407 }
408 }