Skip to content

fix(search): cache paging data before combining, not after - #414

Merged
oxyroid merged 1 commit into
oxyroid:masterfrom
Pierroons:fix/search-paging-crash
Aug 9, 2026
Merged

fix(search): cache paging data before combining, not after#414
oxyroid merged 1 commit into
oxyroid:masterfrom
Pierroons:fix/search-paging-crash

Conversation

@Pierroons

Copy link
Copy Markdown
Contributor

Typing into the search bar kills the process on the first keystroke:

java.lang.IllegalStateException: Attempt to collect twice from pageEventFlow,
which is an illegal operation. Did you forget to call
Flow<PagingData<*>>.cachedIn(coroutineScope)?
    at androidx.paging.PageFetcherSnapshot$pageEventFlow$1.invokeSuspend(PageFetcherSnapshot.kt:82)
    ...
    at androidx.paging.compose.LazyPagingItems$collectPagingData$2.invokeSuspend(LazyPagingItems.kt:168)

Cause

A PagingData taken straight from a Pager may only be collected once.

AppViewModel.channels combines three sources, and combine re-emits the
latest value of every source whenever any single one of them changes.
extensionSearchResults always emits twice per query — an empty placeholder
first, then the results — so the same PagingData reaches the collector a
second time and Paging throws.

cachedIn was already there, but placed after the combine, where it
receives the duplicate rather than preventing it. Moving it onto
localSearchResults makes the stream multicast; filter and
insertHeaderItem still run on every collection, which is what they are meant
to do.

Why it looked like something else

Search appeared to return nothing, so the SQL was the natural suspect — it is
not at fault. The process simply died before any result could be rendered.

Tests

Two, and the second is the point: it builds the same shape without the cache
and asserts that Paging rejects the second collection. A regression here would
otherwise pass a green suite quietly, since the symptom is an empty list rather
than a failure.

a cached stream tolerates the re-emission combine causes
an uncached stream is rejected on that second collection

Verified on device (Android 9, arm64): before, the process died on the first
keystroke; after, a search returns its results and survives several queries in
a row.

Typing in the search bar killed the process on the first keystroke:

  java.lang.IllegalStateException: Attempt to collect twice from
  pageEventFlow, which is an illegal operation.

A PagingData taken straight from a Pager may only be collected once.
combine re-emits the latest value of every source whenever any single one
of them changes, and extensionSearchResults always emits twice per query —
an empty placeholder, then the results — so the same PagingData reached
the collector a second time.

cachedIn was already there but sat after the combine, where it received
the duplicate rather than preventing it. Moving it onto localSearchResults
makes the stream multicast; filter and insertHeaderItem still run on every
collection, which is what they are meant to do.

Searching looked like it simply returned nothing, which is why the SQL was
suspected first — it was never at fault.

Two tests come with it, including one that reproduces the exception when
the cache is absent, so a green suite cannot go quiet on this again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oxyroid
oxyroid merged commit 650d06b into oxyroid:master Aug 9, 2026
1 check passed
@oxyroid

oxyroid commented Aug 9, 2026

Copy link
Copy Markdown
Owner

thanks for your contribution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants