implemented disposal guard in files search controller to safely handle async listener notifications

This commit is contained in:
2026-05-09 23:40:04 +02:00
parent bf28a678c9
commit 15833f3685
@@ -25,6 +25,16 @@ class FilesSearchController extends ChangeNotifier {
bool _serverLoading = false; bool _serverLoading = false;
Object? _serverError; Object? _serverError;
int _serverEpoch = 0; int _serverEpoch = 0;
bool _disposed = false;
/// Guards against the race where the search delegate is closed (and the
/// controller disposed) while a debounced cache scan or server call is
/// still in flight: their late `notifyListeners()` would otherwise throw
/// on a disposed `ChangeNotifier`.
void _safeNotify() {
if (_disposed) return;
_safeNotify();
}
String get query => _query; String get query => _query;
List<String> get pathScope => List.unmodifiable(_pathScope); List<String> get pathScope => List.unmodifiable(_pathScope);
@@ -60,7 +70,7 @@ class FilesSearchController extends ChangeNotifier {
_serverResults = const []; _serverResults = const [];
_serverLoading = false; _serverLoading = false;
_serverError = null; _serverError = null;
notifyListeners(); _safeNotify();
return; return;
} }
// Show loading immediately — even before the (typically fast) cache // Show loading immediately — even before the (typically fast) cache
@@ -68,12 +78,12 @@ class FilesSearchController extends ChangeNotifier {
// starts typing rather than after the first await hop. // starts typing rather than after the first await hop.
_serverLoading = true; _serverLoading = true;
_serverError = null; _serverError = null;
notifyListeners(); _safeNotify();
final cacheHits = await searchLocalCaches(_query, pathScope: _pathScope); final cacheHits = await searchLocalCaches(_query, pathScope: _pathScope);
if (epoch != _serverEpoch) return; if (epoch != _serverEpoch) return;
_cacheResults = cacheHits; _cacheResults = cacheHits;
notifyListeners(); _safeNotify();
_scheduleServerCall(); _scheduleServerCall();
} }
@@ -84,17 +94,17 @@ class FilesSearchController extends ChangeNotifier {
_pathScope = const []; _pathScope = const [];
final epoch = ++_serverEpoch; final epoch = ++_serverEpoch;
if (_query.trim().isEmpty) { if (_query.trim().isEmpty) {
notifyListeners(); _safeNotify();
return; return;
} }
_serverLoading = true; _serverLoading = true;
_serverError = null; _serverError = null;
notifyListeners(); _safeNotify();
final cacheHits = await searchLocalCaches(_query); final cacheHits = await searchLocalCaches(_query);
if (epoch != _serverEpoch) return; if (epoch != _serverEpoch) return;
_cacheResults = cacheHits; _cacheResults = cacheHits;
notifyListeners(); _safeNotify();
_scheduleServerCall(); _scheduleServerCall();
} }
@@ -106,7 +116,7 @@ class FilesSearchController extends ChangeNotifier {
Debouncer.cancel(_debounceTag); Debouncer.cancel(_debounceTag);
_serverLoading = true; _serverLoading = true;
_serverError = null; _serverError = null;
notifyListeners(); _safeNotify();
_runServerCall(); _runServerCall();
} }
@@ -128,18 +138,19 @@ class FilesSearchController extends ChangeNotifier {
.toList(); .toList();
_serverLoading = false; _serverLoading = false;
_serverError = null; _serverError = null;
notifyListeners(); _safeNotify();
} on Object catch (e) { } on Object catch (e) {
if (epoch != _serverEpoch) return; if (epoch != _serverEpoch) return;
_serverResults = const []; _serverResults = const [];
_serverLoading = false; _serverLoading = false;
_serverError = e; _serverError = e;
notifyListeners(); _safeNotify();
} }
} }
@override @override
void dispose() { void dispose() {
_disposed = true;
Debouncer.cancel(_debounceTag); Debouncer.cancel(_debounceTag);
super.dispose(); super.dispose();
} }