Boxlore architecture

July 25, 2026 · View on GitHub

Reference for how the Android app is structured: module boundaries, dependency direction, composition root, and upgrade-safe identity. Module-local detail lives in each folder’s README.md.

Overview

Boxlore is a multi-module Gradle project. :app is the application shell. Shared behavior lives under :core:*. Screen and flow UI lives under :feature:*. There is no Hilt or Koin: a single AppContainer is the composition root.

The graph is layered so playback and features depend inward on catalog and lower cores. Features do not depend on other features. Catalog does not depend on the design system. Feature modules never talk to PostHog directly; they use :core:analytics.

Identity and storage

These values are part of the shipping product. Renames or recreations break upgrades, WorkManager, Media3, and deep links.

ConcernContract
applicationIdcx.aswin.boxlore
Code packages / Android namespacescx.aswin.boxlore.*
SharedPreferences filesOpened as boxlore_*; PrefsFileMigrator copies from legacy boxcast_* when needed. Key strings inside those files stay stable.
DataStoreFile name user_preferences
Room (main)Database filename and tables stay stable
Room (ranking)Separate adaptive database owned by :core:ranking
WorkManagerLegacyWorkerFactory plus permanent core.data.* stubs resolve historical FQCNs
Deep linksboxlore:// and boxcast://, plus both HTTPS share-path prefixes
BuildConfigPrefer BOXLORE_*; Gradle still dual-reads BOXCAST_* fallbacks
Episode / media IDsrss: prefixes, negative RSS IDs, mediaId prefixes, and customCacheKey schemes stay as implemented
Playback instanceOne UI-scoped PlaybackRepository; routes and workers must not construct a second one
Object graph orderDB → PodcastRepositoryQueueRepositoryPlaybackRepositoryQueueManagerSmartDownloadManager
Smart Queue refillOwned by BoxLorePlaybackService only

Module map

:app
:core:model | :core:network | :core:domain | :core:database | :core:prefs
:core:analytics | :core:catalog | :core:rss | :core:downloads | :core:playback | :core:ranking
:core:designsystem | :core:testing
:feature:home | :feature:player | :feature:info | :feature:explore
:feature:library | :feature:onboarding | :feature:briefing

On disk, the folder path matches the Gradle id (core/playback:core:playback).

Ownership

ModuleResponsibilityREADME
:appApplication, AppContainer, navigation host, FCM, WorkerFactoryapp/README.md
:core:modelShared models and enumscore/model/README.md
:core:networkHTTP client (BoxLoreApi / NetworkModule) and network DTOscore/network/README.md
:core:domainThin ports and small result types (no Room or repositories)core/domain/README.md
:core:databaseMain Room database, entities, DAOs, migrationscore/database/README.md
:core:prefsDataStore and SharedPreferences façades (UserPreferencesRepository, BoxcastPrefs)core/prefs/README.md
:core:analyticsAnalytics façade (AnalyticsHelper, Analytics, RecordingAnalytics); PostHog init stays in :app. Event names and properties: docs/ANALYTICS_EVENT_GLOSSARY.mdcore/analytics/README.md
:core:catalogCatalog orchestration: PodcastRepository, subscriptions, content sections, backup/restorecore/catalog/README.md
:core:rssRSS fetch/parse, RssPodcastRepository, rss: / negative IDscore/rss/README.md
:core:rankingAdaptive scoring, LinUCB, feedback, AdaptiveRankingDatabase. Behavior detail: docs/recommendation-system.mdcore/ranking/README.md
:core:downloadsDownloadRepository, Smart Downloads, related WorkManager workerscore/downloads/README.md
:core:playbackPlaybackRepository, queue, Media3 services, smart-queue helperscore/playback/README.md
:core:designsystemTheme and shared composables; no data or network ownershipcore/designsystem/README.md
:core:testingShared fixtures, dispatcher helpers, architecture guardscore/testing/README.md
:feature:homeHome, Settings hub, Add RSS, Debugfeature/home/README.md
:feature:playerPlayer overlay (PlayerSheetScaffold); not a NavHost destinationfeature/player/README.md
:feature:infoPodcast and episode detail, including deep linksfeature/info/README.md
:feature:exploreExplore plus Learn / LearnHistoryfeature/explore/README.md
:feature:libraryLibrary hub, subscriptions, downloads, history, likedfeature/library/README.md
:feature:onboardingFirst-run flows (AI, genre, search, import)feature/onboarding/README.md
:feature:briefingDaily briefing screenfeature/briefing/README.md

Core package roots

ModulePackage
:core:prefscx.aswin.boxlore.core.prefs
:core:analyticscx.aswin.boxlore.core.analytics
:core:rsscx.aswin.boxlore.core.rss
:core:rankingcx.aswin.boxlore.core.ranking
:core:downloadscx.aswin.boxlore.core.downloads
:core:playbackcx.aswin.boxlore.core.playback
:core:databasecx.aswin.boxlore.core.database
:core:catalogcx.aswin.boxlore.core.catalog

Dependency direction

flowchart TB
  app[:app]
  subgraph features [feature]
    home[:feature:home]
    player[:feature:player]
    info[:feature:info]
    explore[:feature:explore]
    library[:feature:library]
    onboarding[:feature:onboarding]
    briefing[:feature:briefing]
  end
  playback[:core:playback]
  catalog[:core:catalog]
  rss[:core:rss]
  analytics[:core:analytics]
  downloads[:core:downloads]
  ranking[:core:ranking]
  prefs[:core:prefs]
  domain[:core:domain]
  database[:core:database]
  network[:core:network]
  design[:core:designsystem]
  model[:core:model]

  app --> features
  app --> playback
  app --> catalog
  app --> downloads
  app --> design
  features --> playback
  features --> catalog
  features --> downloads
  features --> design
  features --> model
  playback --> catalog
  playback --> downloads
  downloads --> catalog
  catalog --> rss
  catalog --> prefs
  catalog --> domain
  catalog --> database
  catalog --> ranking
  catalog --> network
  catalog --> model
  features --> analytics
  features --> ranking
  playback --> ranking
  playback --> analytics
  design --> analytics
  rss --> database
  rss --> domain
  rss --> model
  ranking --> database
  ranking --> domain
  ranking --> prefs
  ranking --> model
  analytics --> model
  prefs --> model
  domain --> network
  domain --> model
  database --> network
  database --> model
  design --> model
  network --> model

The primary runtime stack is playback → catalog → prefs / domain / database / network / model.

Rules enforced in the graph

  • Features do not declare Gradle dependencies on other features (Konsist).
  • Features do not import other feature packages (Konsist).
  • :core:playback depends on :core:catalog; catalog does not depend on playback (Konsist).
  • :core:catalog does not depend on :core:designsystem (Konsist).
  • :core:catalog must not api( :core:analytics or :core:ranking (Konsist; ranking may be implementation only).
  • Features that need analytics or ranking depend on :core:analytics / :core:ranking directly.
  • Feature sources do not import PostHog (scripts/ci/check-feature-no-posthog.sh).
  • No Hilt, Koin, Dagger, or MockK in production sources or Gradle test deps (Konsist).
  • Enums shared by catalog and UI (for example AutoTranscriptState) live in :core:model.
  • :core:domain holds ports; :core:catalog implements catalog-facing ports.
  • :core:network is HTTP/API only. RssFeedClient lives in :core:rss and reaches callers through :core:catalog’s api(rss) edge.
  • getInstance call sites stay on the AppContainer / Room / WorkManager / Calendar / MessageDigest / Firebase allowlist (Konsist).
  • Extracted core modules keep package equal to the module root (permanent core.data.* stubs allowlisted).

Composition root

AppContainer (in :app) owns the shared object graph and is created only in BoxLoreApplication. MainActivity, BoxLoreNavHost, and feature assemblers read application.container. They do not build repositories or a second graph.

Home, Settings, and Info ViewModels are built through assemblers (HomeViewModelAssembler, SettingsViewModelAssembler, InfoViewModelAssembler). Narrow ports under core.domain.ports let ViewModels and workers take fakes without depending on full repositories or BoxLoreDatabase. Production wiring uses RoomLocalCatalog and RoomEpisodeOfflineLookup from AppContainer. ListeningHistoryBackupPort lives in core.catalog.ports.

Product surfaces

SurfaceModuleNotes
Home, Settings hub, Add RSS:feature:homeSettings hosts the RSS dialog
Learn / LearnHistory:feature:exploreLearn is a bottom-nav tab
Player overlay:feature:playerPlayerSheetScaffold, not a NavHost route
Podcast / episode detail:feature:infoDual episode routes and deep links
Playback, queue, Media3:core:playbackIncludes permanent core.data.service stubs
HTTP API:core:networkSeparate from RSS
Ranking:core:rankingOwn adaptive Room database; personalization detail in docs/recommendation-system.md
RSS catalog:core:rssNegative / rss: IDs; exposed through catalog

Upgrade failsafes

Historical FQCNs and preference file names remain reachable so upgrades from older installs keep working. These bridges are permanent.

Workers

Historical FQCNImplementationBridge
cx.aswin.boxcast.core.data.SmartDownloadWorkercx.aswin.boxlore.core.downloads.SmartDownloadWorkerLegacyWorkerFactory
cx.aswin.boxcast.core.data.AutoDownloadWorkercx.aswin.boxlore.core.downloads.AutoDownloadWorkerLegacyWorkerFactory
cx.aswin.boxcast.core.data.PurgeSmartDownloadsWorkercx.aswin.boxlore.core.downloads.PurgeSmartDownloadsWorkerLegacyWorkerFactory
cx.aswin.boxlore.core.data.SmartDownloadWorkercx.aswin.boxlore.core.downloads.SmartDownloadWorkerfactory + stub
cx.aswin.boxlore.core.data.AutoDownloadWorkercx.aswin.boxlore.core.downloads.AutoDownloadWorkerfactory + stub
cx.aswin.boxlore.core.data.PurgeSmartDownloadsWorkercx.aswin.boxlore.core.downloads.PurgeSmartDownloadsWorkerfactory + stub

Services / providers

Historical FQCNImplementationBridge
cx.aswin.boxlore.core.data.service.BoxLorePlaybackServicecx.aswin.boxlore.core.playback.service.BoxLorePlaybackServiceManifest + stub
cx.aswin.boxlore.core.data.service.MediaDownloadServicecx.aswin.boxlore.core.playback.service.MediaDownloadServiceManifest + stub
cx.aswin.boxlore.core.data.service.AutoCollageProvidercx.aswin.boxlore.core.playback.service.AutoCollageProviderManifest + stub

SharedPreferences files

Legacy fileCurrent fileOpened by
boxcast_prefsboxlore_prefsBoxcastPrefs via PrefsFileMigrator
boxcast_theme_fast_cacheboxlore_theme_fast_cacheUserPreferencesRepository
boxcast_analytics_prefsboxlore_analytics_prefsAnalyticsHelper
boxcast_playerboxlore_playerPlaybackRepository / PodcastRepository
android_auto_artwork_sourcesAutoArtworkSourceStore / AutoCollageProvider (Android Auto art URI map; do not rename)
boxcast_api_configboxlore_api_configBoxLoreAppRoot
boxcast_referrer_prefsboxlore_referrer_prefsInstallReferrerManager

ProGuard keeps the permanent core.data.** stubs alongside core.catalog|prefs|analytics|rss|ranking|downloads|playback|database.**.

Source file size

Kotlin sources under */src/main/** stay under 1000 lines. Larger units are split into same-package helpers or composables without changing public APIs or the identity/FQCN contracts above.

Verification

How the tree is tested — commands, coverage floors, and layer status — is in docs/TESTING.md.

LayerRoleWhere
JVM unitLogic, ports, ViewModel slicesdocs/TESTING.md
MaestroDevice E2E smoke (local; YAML validated in CI)maestro/README.md
Architecture-as-codeKonsist and filesystem guards:core:testing
ScreenshotsVisual baselines (optional local Roborazzi)docs/screenshots/README.md

Shared test fixtures live in :core:testing. The project does not use MockK or Hilt.

DocumentContents
docs/MODULE_README_TEMPLATE.mdModule README shape
maestro/README.mdLocal Maestro flows