5.9 KiB
radiOLA — Project Memory
Overview
Android online radio app combining Radio Record (Russian dance radio network) with Radio Browser API (radio-browser.info) for genre filtering across thousands of internet radio stations.
Stack: Kotlin 2.0, Compose BOM 2024.06, Material3, Hilt 2.51.1, Media3 ExoPlayer 1.3.1, Room 2.6.1, Retrofit + Kotlinx Serialization, Coil 2.6.0, DataStore, Lucide icons.
Architecture
Clean Architecture with these layers:
domain/— models (Station,Track,DeeplinkService), repository interfaces, use casesdata/remote/— Retrofit APIs (RecordApi,RadioBrowserApi), DTOs, mappersdata/local/— Room (AppDatabasev2), DAOs (StationDao,TagDao,TrackHistoryDao)data/repository/— impls (StationRepositoryImpl,RadioBrowserRepositoryImpl, etc.)di/— HiltAppModule(two Retrofit instances: Record + Radio Browser)ui/— Compose screens (StationsScreen,HistoryScreen,SettingsScreen,PlayerBottomSheet)service/—PlayerController(ExoPlayer singleton),PlayerService(MediaSessionService)widget/—PlayerWidgetProvider(4×1 home screen widget)
Current State
✅ Working
- Radio Record stations load from
https://www.radiorecord.ru/api/stations/→ cached in Room - Stations grid (
LazyVerticalGrid, 2 columns) with cover, name, favorite heart - Playback — real stream URLs via ExoPlayer, background service, notification, lock screen
- Favorites — Room toggle, context-aware next/prev (all vs favorites only)
- History — auto-saves tracks to
TrackHistoryRepositoryon every_currentTrackchange - Audio devices — pauses on BT/wired/USB removal, resumes on reconnect
- Deeplink buttons — search current track in Yandex, Spotify, etc.
- Genre chips —
FilterChipsrow with music genres only - Radio Browser integration — loads stations by tag when VPN is active
- Icy metadata parsing for Radio Browser streams (
Artist - Songformat)
⚠️ Known Issues / Limitations
- Radio Browser API blocked by some ISPs in Russia (IP
91.98.4.78times out). App detects this and disables Radio Browser features gracefully. Workaround: use VPN. - Radio Record API does not return per-station tags. Tags are global (
StationsResult.tags). We map them to RoomTagEntitytable viaTagDao. - Record station-to-genre mapping is approximate — uses keyword matching on
tooltip/genrefield because API lacks structured genre data per station. - Widget uses
RemoteViewswith limited Compose support;widget_player.xmlavoids AppCompat attrs. - Deprecated
DividerinHistoryScreen.kt:60— should beHorizontalDivider. - KSP
NonExistentClass— clean build required if Hilt constructor params change.
Key Data Flows
Station Loading
StationsViewModel.init→refreshStationsUseCase()→StationRepositoryImpl.refreshStations()RecordApi.getStations()returnsStationsResponse(result = StationsResult(stations, tags))StationDao.insertAll()+TagDao.clearAll()+TagDao.insertAll()StationsViewModel.tagscombinesstationRepository.getTags()+_radioBrowserTags
Playback
PlayerViewModel.play(station)→PlayerController.play(url, prefix, name)- Record source →
getNowPlayingUseCase(id)polls/api/stations/now/every 10s - Radio Browser source → listens to
PlayerController.icyTitle(ExoPlayeronMediaMetadataChanged) _currentTrackupdates → auto-saved to history + lock screen metadata viaupdateMetadata()
Genre Filtering
tag == null→ show all Record stationstag != null→ show Radio Browser stations for tag + Record stations whosegenre/namematches keywords (tagKeywordsmap inStationsViewModel)
Critical Code Rules
ExoPlayer Lifecycle
PlayerController is @Singleton. PlayerService.onDestroy() must NOT call playerController.release() — releasing ExoPlayer kills its internal thread. Subsequent play() calls throw IllegalStateException: dead thread after OS restarts the service.
Room Migrations
AppDatabase is version 2. MIGRATION_1_2 creates tags table. Always add migration + bump version when adding entities.
Radio Browser Null-Safety
RadioBrowserStationDto fields are nullable (urlResolved, tags, country, codec). Mapper falls back: streamUrl = urlResolved ?: url ?: "".
File Inventory
| File | Responsibility |
|---|---|
di/AppModule.kt |
Two Retrofit instances, Room DB with migration, repository bindings |
data/repository/StationRepositoryImpl.kt |
Fetch Record API, cache stations + tags in Room |
data/repository/RadioBrowserRepositoryImpl.kt |
Fetch Radio Browser stations/tags, swallow network errors |
ui/stations/StationsViewModel.kt |
Genre filtering, tag loading, search, Radio Browser availability flag |
ui/stations/StationsScreen.kt |
Grid, chips, search bar, empty state with "Show All" button |
service/PlayerController.kt |
ExoPlayer, audio device callback, Icy title capture, MediaSession wrapper |
service/PlayerService.kt |
Background service, MediaSession, notification, lock screen |
ui/player/PlayerViewModel.kt |
Playback orchestration, now playing polling, Icy parsing, playlist |
data/remote/dto/RadioBrowserDto.kt |
DTOs with nullable fields for Radio Browser API |
data/local/AppDatabase.kt |
Room v2 with StationEntity, TrackHistoryEntity, TagEntity |
Next Steps / Open Items
- Replace deprecated
DividerwithHorizontalDividerinHistoryScreen - Add empty-state illustration or better UX when Radio Browser is unavailable
- Consider caching Radio Browser stations per tag in Room for offline reuse
- Evaluate if
tagKeywordsmapping for Record stations needs tuning per user feedback - Add error retry / exponential backoff for Radio Browser API calls when VPN is unstable