Summary
_MainSearchLoaderState.build() calls prepare() and assigns it to futureVoid on every render β including rebuilds triggered by hot-reload, orientation changes, theme changes, or parent widget updates. This causes multiple concurrent prepare() invocations, state corruption in ActionEntryListService, and unhandled exceptions from fire-and-forget async calls that are never caught.
Affected File
lib/services/main_search_loader.dart
Root Cause
Code (v0.6.2)
@override
Widget build(BuildContext context) {
futureVoid = prepare(); // β CALLED ON EVERY BUILD
return FutureBuilder<dynamic>(
future: futureVoid,
...
);
}
The Flutter docs explicitly state that FutureBuilder's future must not be created inside build() β it must be stored and only initialized once (in initState()). Every call to build() creates a new Future, which:
- Restarts
prepare() while the previous invocation is still running
- Clears
ActionEntryListService again mid-load via ActionEntryListService.clearEntries()
- Causes the loading spinner to flash/reset on any widget rebuild
- On slower systems, causes race conditions in entry loading
Secondary Bug: Fire-and-forget async calls with no error handling
// All of these are fire-and-forget with no .catchError():
Linux.getAllFolderEntriesOfUser(context);
Linux.getAllAvailableApplications();
Linux.getRecentFiles(context);
Linux.getFavoriteFiles(context);
Linux.getBrowserBookmarks(context);
Linux.getUninstallEntries(context);
Any exception thrown inside these async functions is silently swallowed β it becomes an unhandled Future exception that only shows up in debug logs, never to the user. On systems where one of these functions fails (e.g. browser bookmarks not found, recent files db locked), the app loads partially with no feedback.
Fix
1. Move prepare() to initState()
@override
void initState() {
super.initState();
futureVoid = prepare(); // Only called ONCE
}
@override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: futureVoid, // Stable reference
...
);
}
2. Add error handling to fire-and-forget calls
Linux.getAllFolderEntriesOfUser(context)
.catchError((e) { print('Error loading folders: $e'); return []; });
Linux.getAllAvailableApplications()
.catchError((e) { print('Error loading applications: $e'); return []; });
// ... etc
Severity
P1: prepare() being called on every build() is a fundamental Flutter anti-pattern that causes guaranteed state corruption on any widget rebuild. This can manifest as:
- Infinite loading spinner if a rebuild happens mid-load
- Empty search results after a theme/locale change
- Double-loading of all entry categories on startup
P2: Silent swallowing of async errors means broken functionality (empty results) with no user feedback and no logs.
Summary
_MainSearchLoaderState.build()callsprepare()and assigns it tofutureVoidon every render β including rebuilds triggered by hot-reload, orientation changes, theme changes, or parent widget updates. This causes multiple concurrentprepare()invocations, state corruption inActionEntryListService, and unhandled exceptions from fire-and-forget async calls that are never caught.Affected File
lib/services/main_search_loader.dartRoot Cause
Code (v0.6.2)
The Flutter docs explicitly state that
FutureBuilder'sfuturemust not be created insidebuild()β it must be stored and only initialized once (ininitState()). Every call tobuild()creates a newFuture, which:prepare()while the previous invocation is still runningActionEntryListServiceagain mid-load viaActionEntryListService.clearEntries()Secondary Bug: Fire-and-forget async calls with no error handling
Any exception thrown inside these async functions is silently swallowed β it becomes an unhandled
Futureexception that only shows up in debug logs, never to the user. On systems where one of these functions fails (e.g. browser bookmarks not found, recent files db locked), the app loads partially with no feedback.Fix
1. Move prepare() to initState()
2. Add error handling to fire-and-forget calls
Severity
P1:
prepare()being called on everybuild()is a fundamental Flutter anti-pattern that causes guaranteed state corruption on any widget rebuild. This can manifest as:P2: Silent swallowing of async errors means broken functionality (empty results) with no user feedback and no logs.