Case Study · Personal Project
Tunify
A free radio app that brings 65,000+ FM, AM, internet and live stations from 200+ countries into one modern, offline-first Android experience — built solo as a playground for cutting-edge Jetpack.
01 The problem
Radio apps tend to fall into two traps: they either wrap a single streaming URL in a thin UI, or they fetch everything on demand and fall apart the moment the network hiccups. I wanted Tunify to feel like a proper product — instant to browse, resilient offline, and effortless across phones, foldables and the TV.
The core challenges:
- Scale & browsing — 65,000+ stations across countries, languages and genres need to page smoothly without janky loads or memory blowups.
- Offline-first — the last browsed lists should be there instantly on reopen, even with no connection.
- Unreliable streams — internet radio streams drop, stall and change codecs; the player has to recover gracefully.
- Reach everywhere — one codebase that adapts to compact phones, large screens and casts to Chromecast.
02 Architecture
Tunify is a multi-module clean-architecture app — one
feature module and eleven focused core modules under a thin
app shell. Each core does exactly one job (player, database,
network, design system…), so features depend only on the contracts they
need. It keeps build times fast, boundaries honest, and every piece
independently testable.
The presentation layer runs on Navigation 3 and the Adaptive Navigation Suite — both barely out of alpha — so the same screens reflow between compact phones and large/foldable displays instead of being locked to one form factor.
03 Deep-dive: offline-first paging
Browsing 65,000+ stations is a paging problem, and doing it offline-first
is a caching problem. Tunify solves both with Paging 3's
RemoteMediator and Room as the single
source of truth. The UI never reads from the network directly —
it only ever observes the database. Ktor fills the database in the
background; Room streams it to Compose.
There are five mediators — for the main list plus browsing by country, by language, and the country/language catalogs themselves — each backed by the free Radio Browser API.
- Instant reopen — the last lists render from Room before the network is even touched.
- Consistent UI — one code path (DB → UI) whether online or off; no branching on connectivity in the composables.
- Bounded memory — Paging windows the data, so a 65k list never fully materialises.
04 Deep-dive: the player
Playback lives in its own :core:player module, composed of
small "player blocks" behind clean interfaces rather than one giant
service. Media3 / ExoPlayer drives audio; a media-session
callback wires up notifications, lock-screen and Bluetooth controls; and a
dedicated error handler keeps flaky internet-radio streams from taking the
app down.
- Device player — Media3 ExoPlayer with a media-session notification provider for background & lock-screen control.
- Casting — a
CastPlayerManagerhands off to Chromecast and back without the UI caring which player is active. - Sleep timer — a manager that fades playback out after a chosen duration.
- Strategies & error handling — pluggable playback strategies and a
PlaybackErrorHandlerthat recovers from stream drops and codec changes.
Splitting the player into swappable managers (device / cast / timer) behind interfaces meant Chromecast support dropped in as another manager — the screens never learned a new API.
05 Technical decisions
- Ktor over Retrofit — a lighter, coroutine-native client that keeps the network module Kotlin-first and multiplatform-ready.
- Room as source of truth — the backbone of offline-first; the network is a cache-filler, not a UI dependency.
- Navigation 3 + Adaptive Navigation — betting early on Google's next-gen nav to get real adaptive layouts across form factors.
- Hilt everywhere — every module exposes its own DI bindings, so wiring stays local and features compose cleanly.
- A design-system module — Material 3 typography and a responsive
TunifyDimentoken system, so spacing and type scale consistently on every screen size.
06 Why I built it & what's next
I built Tunify for two reasons. The first is simple: I love radio — the serendipity of stumbling onto a station from the other side of the world. The second is that I wanted a real, shippable product to push the newest of Android on, not a toy demo. Tunify became my proving ground for Navigation 3, the Adaptive Navigation Suite, Media3 and a strict offline-first architecture — the tools I believe most Android apps will standardise on next. Building it end-to-end, solo, forced every decision to be one I'd defend in a production codebase.
What I'd tackle next: richer discovery (genre/mood), a "recently played" history, and expanding the adaptive layouts to a proper TV experience.
- Kotlin
- Jetpack Compose
- Material 3
- Navigation 3
- Adaptive Navigation
- Paging 3 · RemoteMediator
- Room
- Ktor
- Media3 / ExoPlayer
- Chromecast
- Hilt
- DataStore
- Coroutines & Flow
- Coil
- Multi-module