perf(mailbox): page the unified "All inboxes" list (#124)

The unified inbox query (WHERE folder = ?, no accountId) has no folder-leading
index, so it scans in timestamp order and materializes the whole unified inbox
(~4k rows at a 20k cache) into memory on every emission. Apply Paging 3 to the
unified browse path so query, mapping, and recomposition cost scale with the
visible window, not the total cache.

- MessageDao.pagingUnifiedFolderSummaries: a PagingSource over the folder's
  synced rows (inInbox = 1); unified search keeps the whole-folder query so it
  can still surface transient server-search hits.
- MailRepository.pagedUnifiedFolderMessages: a Pager (pageSize 40, initialLoad
  120, no placeholders) mapping summaries to domain.
- MailboxViewModel.pagedMessages: paged while browsing the unified inbox, else
  empty; the messages list flow stays empty in that state so the whole cache is
  never materialized. Selection captures each row's accountId at tap time, so
  "Move" still resolves the selection's account without an in-memory list.
- MailboxScreen renders the unified browse list via collectAsLazyPagingItems;
  per-account and search views render the flat list unchanged (issue #86 stays
  flat).

Profiling (docs/perf/issue-124-unified-inbox-paging.md) on an api29 emulator:
current whole-inbox first-emit ~24.6 ms at a 20k cache vs. the paged first page
~6.8 ms and flat regardless of cache size (~3.6x). EXPLAIN QUERY PLAN shows the
paged query still stops early on the existing timestamp index, so no
(folder, ...) index and no schema migration are added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 08:49:52 -05:00
co-authored by Claude Fable 5
parent 48333ab01e
commit f8d03a4343
12 changed files with 376 additions and 50 deletions
+10
View File
@@ -12,6 +12,7 @@ webkit = "1.12.1"
biometric = "1.1.0"
composeBom = "2026.06.00"
room = "2.8.4"
paging = "3.3.6"
sqlcipher = "4.16.0"
datastore = "1.2.1"
work = "2.11.2"
@@ -72,9 +73,18 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
# Room MigrationTestHelper (instrumented migration tests).
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
# Room PagingSource support — lets @Query methods return androidx.paging.PagingSource.
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "room" }
# SQLCipher — opt-in at-rest encryption of the Room cache.
sqlcipher-android = { group = "net.zetetic", name = "sqlcipher-android", version.ref = "sqlcipher" }
# Paging 3 — pages the unified "All inboxes" list so its query/recomposition cost scales with the
# visible window, not the whole cache (issue #124). runtime = Pager/PagingData; compose =
# collectAsLazyPagingItems; testing = asSnapshot for JVM unit tests.
androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
androidx-paging-testing = { group = "androidx.paging", name = "paging-testing", version.ref = "paging" }
# DataStore (settings) / WorkManager (sync) — wired in later increments
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }