Returning from the reader/message screen to the inbox briefly showed the "No messages" empty state and reloaded (a multi-second stall). The inbox's only Paging presenter — collectAsLazyPagingItems in MailboxScreen — is torn down the instant a message is opened, so the cachedIn(viewModelScope) pager in MailboxViewModel (MailboxViewModel.kt, pagedMessages) loses its downstream collector. Opening an unread message writes setRead, which invalidates the Room PagingSource; with nothing collecting the cached flow, the fresh generation only cold-loaded once the inbox re-entered composition — presenting itemCount == 0 for a frame on return.
The empty-state gate (itemCount == 0 && refresh is NotLoading && append.endOfPaginationReached) already landed with #214/#223; this ticket's remaining scope was the structural cold-reload.
Fix
Add an always-on, invisible PagingDataPresenter<Message> in MailboxViewModel that stays subscribed to the cached paged flow across the reader visit:
collectLatest hands each new generation (including the one produced when setRead invalidates the source) to collectFrom, which drives its initial load in the background while the reader is up. The return then replays a full window with zero empty frames. The presenter renders nothing — LazyPagingItems still drives the visible list.
Tests
MailboxViewModelTest (unit): a real, invalidatable Pager proves the pager loads its initial window and reloads after invalidate() with no UI collector attached — i.e. the ViewModel's keep-alive alone drives both loads.
MailboxScreenTest (instrumented): the empty state is held back while refresh is Loading and shown only once the pager settles genuinely empty, driven via PagingData.from(..., LoadStates) through a new FakeMailRepository paged override.
Validation
:app:testDebugUnitTest (incl. the new keep-alive test) + :app:compileDebugAndroidTestKotlin + :app:lintDebug + :app:ktlintCheck + :app:detekt all green locally (JDK 21). The instrumented suite is deferred to CI (local emulator was offline/peer-owned; per repo policy a second emulator was not booted).
Closes #219
## Root cause
Returning from the reader/message screen to the inbox briefly showed the **"No messages"** empty state and reloaded (a multi-second stall). The inbox's only Paging presenter — `collectAsLazyPagingItems` in `MailboxScreen` — is torn down the instant a message is opened, so the `cachedIn(viewModelScope)` pager in `MailboxViewModel` (`MailboxViewModel.kt`, `pagedMessages`) loses its downstream collector. Opening an *unread* message writes `setRead`, which invalidates the Room `PagingSource`; with nothing collecting the cached flow, the fresh generation only cold-loaded once the inbox re-entered composition — presenting `itemCount == 0` for a frame on return.
The empty-state *gate* (`itemCount == 0 && refresh is NotLoading && append.endOfPaginationReached`) already landed with #214/#223; this ticket's remaining scope was the structural cold-reload.
## Fix
Add an always-on, invisible `PagingDataPresenter<Message>` in `MailboxViewModel` that stays subscribed to the cached paged flow across the reader visit:
```kotlin
viewModelScope.launch {
pagedMessages.collectLatest { keepAlivePresenter.collectFrom(it) }
}
```
`collectLatest` hands each new generation (including the one produced when `setRead` invalidates the source) to `collectFrom`, which drives its initial load in the background while the reader is up. The return then replays a full window with zero empty frames. The presenter renders nothing — `LazyPagingItems` still drives the visible list.
## Tests
- **`MailboxViewModelTest`** (unit): a real, invalidatable `Pager` proves the pager loads its initial window **and** reloads after `invalidate()` with **no UI collector attached** — i.e. the ViewModel's keep-alive alone drives both loads.
- **`MailboxScreenTest`** (instrumented): the empty state is held back while `refresh` is `Loading` and shown only once the pager settles genuinely empty, driven via `PagingData.from(..., LoadStates)` through a new `FakeMailRepository` paged override.
## Validation
`:app:testDebugUnitTest` (incl. the new keep-alive test) + `:app:compileDebugAndroidTestKotlin` + `:app:lintDebug` + `:app:ktlintCheck` + `:app:detekt` all green locally (JDK 21). The instrumented suite is deferred to CI (local emulator was `offline`/peer-owned; per repo policy a second emulator was not booted).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #219
Root cause
Returning from the reader/message screen to the inbox briefly showed the "No messages" empty state and reloaded (a multi-second stall). The inbox's only Paging presenter —
collectAsLazyPagingItemsinMailboxScreen— is torn down the instant a message is opened, so thecachedIn(viewModelScope)pager inMailboxViewModel(MailboxViewModel.kt,pagedMessages) loses its downstream collector. Opening an unread message writessetRead, which invalidates the RoomPagingSource; with nothing collecting the cached flow, the fresh generation only cold-loaded once the inbox re-entered composition — presentingitemCount == 0for a frame on return.The empty-state gate (
itemCount == 0 && refresh is NotLoading && append.endOfPaginationReached) already landed with #214/#223; this ticket's remaining scope was the structural cold-reload.Fix
Add an always-on, invisible
PagingDataPresenter<Message>inMailboxViewModelthat stays subscribed to the cached paged flow across the reader visit:collectLatesthands each new generation (including the one produced whensetReadinvalidates the source) tocollectFrom, which drives its initial load in the background while the reader is up. The return then replays a full window with zero empty frames. The presenter renders nothing —LazyPagingItemsstill drives the visible list.Tests
MailboxViewModelTest(unit): a real, invalidatablePagerproves the pager loads its initial window and reloads afterinvalidate()with no UI collector attached — i.e. the ViewModel's keep-alive alone drives both loads.MailboxScreenTest(instrumented): the empty state is held back whilerefreshisLoadingand shown only once the pager settles genuinely empty, driven viaPagingData.from(..., LoadStates)through a newFakeMailRepositorypaged override.Validation
:app:testDebugUnitTest(incl. the new keep-alive test) +:app:compileDebugAndroidTestKotlin+:app:lintDebug+:app:ktlintCheck+:app:detektall green locally (JDK 21). The instrumented suite is deferred to CI (local emulator wasoffline/peer-owned; per repo policy a second emulator was not booted).🤖 Generated with Claude Code