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
}