Skip to content

Architecture

Overview

Voice is built on a modular, layered architecture. The design separates infrastructure, shared domain logic, and user-facing features. This modularization improves build times, testability, and maintainability, while keeping feature ownership clear.

At a high level:

  • Infrastructure modules provide the app entry point, build tooling, and navigation.
  • Core modules encapsulate reusable domain and service logic (playback, scanning, data, logging, etc.).
  • Feature modules implement user-facing screens, composed from core logic and UI components.
  • Feature flags are defined per-feature and implemented via :core:featureflag, sourced through remote config.

Infrastructure

  • :app – Application entry point, dependency injection graph, and product flavor wiring (free / play)
  • :navigation – Navigation abstractions and route definitions
  • plugins/ – Included Gradle build with convention plugins
  • scripts/ – Build and utility scripts

Core (Shared Logic)

:core:* modules hold reusable services and abstractions: data and storage, playback, scanning, search, logging, analytics, remote config, feature flags, localized strings, and shared Compose UI and theming.

Features

:features:* modules are screen- or flow-based. Each owns its UI (Compose) and presentation logic and delegates to :core modules for data and services.

API / Implementation Splits

When a contract has several implementations, it is split into an :api module plus implementation modules (for example noop, firebase, play, free, or impl). Consumers depend only on :api; :app picks the implementation, often per product flavor.

Dependency Flow

  • Features depend only on :core and :navigation, never on other features.
  • :core modules depend on each other as needed, but never on features.
  • Only :app depends on implementation modules. Everything else depends on the :api module, tests may use implementations.
  • :app wires everything together at runtime.

The build fails when a module breaks these rules, see plugins/src/main/kotlin/checkModuleDependencies.kt.

This ensures unidirectional dependency flow:

:app → Features → :navigation → Core

Diagram

flowchart LR
  subgraph Infrastructure
    app(":app")
    navigation(":navigation")
  end

  subgraph Core
    core(":core")
  end

  subgraph Features
    features(":features")
  end

  app --> navigation
  app --> features
  app --> core
  features --> core
  features --> navigation
  navigation --> core

Tech Decisions

  • Compose (UI) – Declarative UI with Material 3 for consistency and accessibility
  • Metro (DI) – Lightweight dependency injection across modules
  • Navigation3 – Type-safe, modular navigation
  • ExoPlayer (Media3) – Robust audio playback engine
  • Room – Persistent storage
  • Kotlin Serialization – JSON parsing and object serialization
  • Coil – Efficient image loading

Module Lifecycle

  1. Add new functionality as a feature module.
  2. Extract reusable logic into :core modules once multiple features need it.
  3. Keep infrastructure minimal — mainly for wiring and build configuration.