Files
subtracks/docs/implementation-plan.md
T
austinried c48ec670d8 address arch review: guard the page cap, coalesce syncs, tighten buffers
- Paged loops now throw when they hit maxPages with a full last page, so a
  truncated enumeration aborts the entity's collect and skips its prune
  instead of deleting rows the sync never saw. The cap is injectable for
  tests.
- Move sync serialization into SyncManager (tryLock coalesces duplicate
  requests) and construct SyncService there; SourceRepository only exposes
  the active MusicSource. Removes the repo <-> sync import cycle.
- Re-throw CancellationException instead of reporting a cancelled sync as
  failed.
- Add buffer(1) to the outer albums/songs/playlistSongs flows so their
  flowOn channel cannot hold 64 batches.
- Rank prefetch is best-effort (missing frequent/recent no longer aborts
  the album sync).
- Skip playlist entries without an id, keeping positions contiguous.
- Chunk stale-disc deletes like the other prunes.
- Update the implementation plan line that still described the old
  single-transaction sync.
2026-10-04 00:29:58 +00:00

4.1 KiB

Implementation plan

The rewrite is delivered in vertical slices. Each phase leaves the app in a working, testable state.

Phase 0 - Foundation (done)

Nix devshell (JDK, Gradle, Android SDK, nushell, navidrome, gonic), Gradle Kotlin DSL at the repo root, a Compose skeleton, Roborazzi/Robolectric screenshot testing, and the carried-over launcher and notification assets. The app ships as com.subtracks.next.

Phase 1 - Data foundation (done)

Everything needed to mirror a server library locally:

  • MusicSource abstraction with SubsonicClient, SubsonicXml and SubsonicSource.
  • Room 3 schema on bundled SQLite, so the SQLite version is identical on every device.
  • SyncService: stream each entity in batches, upserting per batch and pruning the IDs the server no longer lists; the sync is not atomic.
  • Unit tests (client, XML, sync), native integration tests against navidrome and gonic, and CI (unit, lint, integration) with warm caches.

Phase 2 - UI vertical slice (done)

Goal: a usable app for browsing a server, end to end.

  • Koin DI and DataStore-backed preferences (album sort); the unused app_settings Room table was removed in favour of DataStore.
  • App shell: a root gate that shows the add-server flow until a server exists, then the tabbed library screen matching the Flutter app (icon-only tabs for albums, artists, songs and playlists, with sync and settings actions), plus album and playlist detail screens and a monochrome theme.
  • Source setup: name/address/username/password with token-auth toggle, a connection test, save-and-sync, plus server switching and removal in settings.
  • Library browsing: albums (covers-only grid), artists, songs and playlists paged from Room (PagingSource) and rendered with LazyPagingItems, with Coil cover art served from stable media URLs.
  • Artist detail: tapping an artist opens their albums, newest first, and an album can be opened from there.
  • Roborazzi screenshots for the new screens, rendered on demand for review (not committed).

Not done yet in this slice (deliberately): cover-art tonal colour extraction, long-press menus, editing an existing server, and per-section sort/filter controls (only album sort is wired).

Phase 3 - Playback (in progress)

  • (done) Media3/ExoPlayer playback service, queue and now-playing UI: a MediaSessionService plus a mini player and a Now Playing screen.
  • (done) Media notifications and headset/Bluetooth controls, from the media session.
  • (done) A SQL-backed queue of references (whole playlist / album / song, each with an optional ordinal range) resolved lazily, with only a bounded window in memory and in the player.
  • (done) Streaming bitrate and preferred format from settings (maxBitRate/format, so the server transcodes as configured).
  • (done) Playing from the Songs tab: the tapped song queues the whole songs list at that position.
  • (done) A queue view that lists the resolved queue, jumps to a track on tap, removes tracks and reorders them by dragging, with a one-step undo.
  • Adding to the queue from the library (play next / add to queue), and gapless format preferences.
  • Scrobbling and "now playing" back to the server.
  • Optional download/caching.

Phase 4 - Search, playlists, offline

  • (done) Search: a substring filter on the library queries, scoped to the active tab, with a docked search field and per-tab sort/filter controls.
  • Playlist browsing and editing (create/add/remove), syncing changes back to the server.
  • Offline mode and downloads.

Phase 5 - Release

  • Change applicationId from com.subtracks.next to com.subtracks.
  • Signing, versioning and Fastlane metadata.
  • F-Droid submission. Their buildserver must have Gradle >= 9.6 and SDK 37, which this project targets; do not rely on the Gradle wrapper there (fdroidserver deletes gradlew and uses its own Gradle).

Deferred decisions

  • Whether to drop the plaintext-auth fallback or keep it behind an "insecure server" toggle.
  • Robolectric runs at SDK 35 while targetSdk is 37; aligning them changes the on-demand Roborazzi renders (nothing committed to re-record).
  • Whether to extract :core:data and friends once the UI has grown.