Back to devrath.me

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.

13Gradle modules
65k+Radio stations
200+Countries
100%Jetpack Compose

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:

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.

:app :features:radio data · domain · presentation :core — 11 focused modules :core:player Media3 · Cast · timer :core:network Ktor client :core:database Room :core:model domain models :core:preferences DataStore designsystem Material 3 tokens :core:ui shared composables :core:location :core:appupdate logger · common Dependencies point inward: features → core contracts. No core depends on a feature. app feature core
13-module dependency graph — one feature, eleven single-purpose core modules.
Ahead of the curve

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.

Compose UI LazyColumn Pager PagingData flow Room source of truth RemoteMediator ×5 Ktor client Radio Browser API observe query fetch upsert page
The UI observes Room; the mediator fetches over Ktor and upserts pages. Offline, Room simply serves what it already has.

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.

Design choice

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

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.

Back to portfolio