Skip to content

Architecture

Utsha Basak edited this page Oct 11, 2026 · 2 revisions

Architecture

The app follows Android's recommended architecture: a single activity, Jetpack Compose screens, ViewModels with unidirectional data flow, and an offline-first data layer. It is split into Gradle modules so each part builds and tests on its own.

Modules

flowchart TD
    app[app] --> features
    subgraph features[feature]
        home & catalogue & book & shop & wishlist & cart & checkout & auth & settings
    end
    features --> ui[core:ui]
    features --> data[core:data]
    ui --> designsystem[core:designsystem]
    data --> network[core:network]
    data --> database[core:database]
    data --> datastore[core:datastore]
    data --> model[core:model]
    network --> model
Loading
Module Holds
app MainActivity, the five-tab Navigation 3 shell, Hilt setup, flavours and the updater
feature:* One screen or flow each, with its ViewModel and strings
core:ui Shared composables (book cards, prices, loading and error states), the routes and the website's links
core:scanner The ISBN barcode scanner: CameraX for the picture, ZXing to read it, all on the phone
core:designsystem The theme: the website's colours, Plus Jakarta Sans, light and dark
core:data Repositories, the session manager and the network monitor
core:network The Retrofit API, DTOs, mappers and OkHttp interceptors
core:database Room: the offline cache, recent searches and recently viewed books
core:datastore DataStore settings and the Keystore-encrypted session store
core:model Plain Kotlin models used by every layer

Features never depend on each other. They navigate through the routes in core:ui, and the app module wires them together.

Data flow

  1. A screen collects its ViewModel's StateFlow of UI state and calls the ViewModel's functions for events.
  2. The ViewModel asks a repository, which returns an Outcome (success or a typed error) rather than throwing.
  3. Repositories serve the saved copy from Room at once, then refresh it from the API. Account data is saved under the account's id and removed on sign-out.

Navigation

Navigation 3 with @Serializable routes. Each of the five tabs has its own back stack; Home's stack sits under the current tab's, so Back from another tab's first screen returns to Home. Sign-in and checkout hide the tab bar.

Sessions and security

  • The access token lives in memory only.
  • The refresh token is encrypted with AES-256-GCM using a key in the Android Keystore (StrongBox where the phone has one).
  • Each renewal is signed by a P-256 key created on the phone, which never leaves it. The server ties the session to that key, so a copied refresh token is useless elsewhere.
  • An OkHttp authenticator renews the token once when the server answers 401; concurrent requests share one renewal.
  • Cloud backups and device transfers leave the session and the cache behind.

The API

The app uses the website's API under /api/v1, defined in the website repository's server/shared/api.d.ts. App-specific endpoints live under /api/v1/app: configuration (sign-in options, delivery charges, the latest release) and sign-in.

Every request carries X-Client: android/<versionCode>. If the server ever stops supporting a build, it answers 426 and the app shows an update screen. The website's docs/API-VERSIONING.md describes the rules both sides follow.

Images

Coil loads covers through the same OkHttp client as the API, sharing its connection pool and disk cache, and asks Cloudinary for a size that fits the screen.

Clone this wiki locally