Flutter / Editorial solution guide

Stop stale results after search or logout

Reproduce out-of-order completions and protect both request freshness and session ownership.

About 3 min readContent reviewed

Read-only knowledge base · No public comments

Recognize the symptoms

Search B appears correctly, then a slower response for search A replaces it. In another case, a request from the previous account populates a screen after logout and login.

A mounted check is already present, so no setState-after-dispose error occurs even though the displayed data belongs to obsolete inputs.

Understand the likely causes

  • Mounted verifies owner lifetime, not which query or session owns a result. Successful completion is not automatically permission to commit.
  • Only the success branch may be guarded, allowing an obsolete failure to replace newer success. A global repository cache may also mix data from different account scopes.

Record input and owner identity safely

Use synthetic request tickets, coarse query labels, and session generations to reconstruct the ordering. Avoid logging complete personal queries, credentials, or response payloads. Distinguish request start from completion and UI commit. If A starts before B but completes after B, that is normal asynchronous behavior; the defect is the policy allowing A to commit after it becomes obsolete. Also check whether the same State survives route changes or account switching, since its continued existence can make a lifetime-only guard appear successful.

Declare the intended command policy

For a typeahead search, latest input usually owns the visible result. For a transactional save, replacing an older operation may be inappropriate because it can already have changed remote state. Choose replacement, rejection, queuing, or reconciliation based on the task. Debouncing reduces how often a search begins but does not guarantee completion order for operations already started. Cancellation can save resources where supported, yet a late callback or uncertain server completion still needs an ownership rule. Keep those mechanisms separate in both implementation and tests.

Check both generations before committing

Capture the current session generation and a fresh request ticket when the operation begins. On completion, compare both against their current values as well as the State lifetime. The same checks belong in the failure branch. The fragment intentionally shows only the gate; the surrounding model must own transitions and invalidation. Increment the session generation when account ownership changes, and scope caches accordingly. Do not merely hide the old screen while its shared state can still notify a new user’s view.

dart Illustrative Dart or Flutter fragment; requires the declared SDK host.
final requestTicket = ++requestGeneration;
final sessionTicket = sessionGeneration;
final next = await repository.search(query);
if (!mounted || requestTicket != requestGeneration ||
    sessionTicket != sessionGeneration) return;
setState(() { results = next; });
// Apply the same identity gate before committing an error.
Expected output
Only a completion matching the current request and session may commit.

This is a State fragment, not cancellation and not a server authorization mechanism. No request is executed here.

Build a deterministic reproduction

Provide a fake repository with separately controlled pending responses. Start A, start B, complete B successfully, and complete A last. Assert that B remains current. Then repeat with A failing, with disposal before completion, and with logout followed by a new session before A resolves. These cases require no live service or arbitrary sleep. If the model caches results, assert the cache key includes the correct account scope and that obsolete private data cannot be published through another listener after the UI gate rejects it.

Verify the fix

The newer success survives the older success and older failure; disposed owners receive no commits; account switching invalidates results and cache publication from the previous session.

Report these as application-policy tests when using fakes. Native cancellation, transport termination, and server-side authorization require their own integration evidence.

Keep it from recurring

  • Keep lifetime, freshness, cancellation, and authorization as explicit separate contracts. Reuse a small deferred-response fixture and require every asynchronous success and error path to apply the same ownership policy.

Official references