Edda
Senior Android Engineer · Tapforce · 2025–present
- Kotlin
- Jetpack Compose
- Koin
- Ktor
- Media3
- Supabase
View on Google Play: open play://com.eddamedia.edda
Context
Edda is an audiobook player — browse and discover books by genre, stream them, and keep a personal bookshelf you actually get through. It has a social side too: readers share and receive book recommendations and highlights. But the hard part of an audiobook app is rarely the screens; it is everything that has to keep working while the screen is off.
Approach
Edda is a single Gradle module split by feature at the package level, with each feature internally layered into data, domain, and presentation, and a core package holding the shared infrastructure. The deliberate decision is what isn't there: no book kernel. The obvious design — one big package owning everything about a book — was decomposed instead into catalog, player, bookshelf, shares, comments, voice, and stats, siblings over shared core models, alongside auth, search, user, notifications, and onboarding. Koin wires it together, every feature shipping its own DI module, and the boundaries are machine-enforced: a Konsist test asserts core never imports a feature and that every feature-to-feature edge is on a curated allowlist. Built greenfield, it took that shape from the start.
Technical decisions
Edda revolves around one question — where is the listener right now — so that lives in a single immutable PlaybackState: current chapter, position, speed. Anything that can be derived is derived rather than stored — progressFraction is computed from position and duration — so a progress bar can never drift out of sync with the player that owns the truth.
data class PlaybackState(
val isPlaying: Boolean = false,
val isBuffering: Boolean = false,
val currentChapterId: Int? = null,
val currentPosition: Long = 0L,
val duration: Long = 0L,
val playbackSpeed: Float = 1.0f,
val seekGeneration: Int = 0
) {
val progressFraction: Double?
get() = if (duration > 0L) currentPosition.toDouble() / duration.toDouble() else null
}The player itself is a MediaSession-backed Media3 service — but the interesting pipeline is streaming TTS. Chapters with no pre-rendered audio are synthesized on the fly: transcript batched to a text-to-speech edge function, per-batch items appended to the live player as they land. The hard parts — batch planning, alignment, the word-timing math — are extracted into pure domain classes with no Android dependencies, unit-tested in isolation from the streaming machinery that uses them.
Outcome
Edda ships through a real pipeline — CI runs Spotless, detekt (with a custom rule module), unit tests, and lint on every PR, and a versionCode bump gates a signed release to the Play Store. It has moved through sixty-two builds to its current 1.11, across five hundred and forty-nine commits, nearly all mine. What I was watching for early was whether the feature-based structure would slow things down as the app filled out — fifteen feature packages later it has not, and when a security review moved voice cloning behind edge functions and dropped a client-side API key, the seams made it a contained change rather than surgery.