Skip to content

πŸ› [Crash] main_search_loader.dart: prepare() re-invoked on every build() call causing state corruption and unhandled async exceptionsΒ #272

Description

@Toqsick

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:

  1. Restarts prepare() while the previous invocation is still running
  2. Clears ActionEntryListService again mid-load via ActionEntryListService.clearEntries()
  3. Causes the loading spinner to flash/reset on any widget rebuild
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions