Radio Hangi: ვებ-აპიდან native Android აპლიკაციამდე Claude Code-ით
ჩემს მთავარ რადიოს — Radio Hangi-ს — უკვე ჰქონდა ვებ-ვერსია. მუშაობდა, უკრავდა, კარგად გამოიყურებოდა. მაგრამ ვებ-აპი ვებ-აპად რჩება: ეკრანი რომ ჩაქრება, მუსიკაც ჩერდება, lock-screen-ზე კონტროლი არ გაქვს, home screen-ზე widget-ს ვერ ჩასვამ.
ამიტომ გადავწყვიტე native Android აპლიკაცია გამეკეთებინა — სუფთა Kotlin, Jetpack Compose, Media3. და მთელი ეს გზა Claude Code-თან ერთად გავიარე.
ეს პოსტი იმაზეა, თუ როგორ იშლება ერთი დიდი იდეა მართვად ნაბიჯებად — და რატომ არის კარგად დაწერილი prompt ნახევარი სამუშაო.
იდეა: ორი ეკრანი, ორი სხვადასხვა სამყარო
დასაწყისშივე ერთი რამ მქონდა გარკვევით:
- Screen A — Radio Hangi (მთავარი): ერთი ფიქსირებული live stream. აქ მნიშვნელოვანია now-playing metadata — სიმღერის სათაური, შემსრულებელი, album art, რომელიც ავტომატურად იცვლება, და lyrics.
- Screen B — World Radio: ათასობით სადგური Radio Browser API-დან. აქ album art-ი და lyrics არ არსებობს — მხოლოდ სადგურის favicon.
ეს განსხვავება კრიტიკული იყო. ორ ეკრანს სრულიად განსხვავებული data model აქვს, და ყველაზე ხშირი შეცდომა სწორედ მათი არევაა.
საიდუმლო: prompt, რომელიც ნამდვილად აზროვნებს
Claude Code-ს არ მივწერე “გამიკეთე რადიო-აპი”. ამის ნაცვლად დავწერე დეტალური ტექნიკური დავალება — თითქოს senior Android engineer-ს ვუსახავდი task-ს:
# ROLE
You are a senior Android engineer specializing in media-playback apps,
with deep expertise in Kotlin, Jetpack Compose, Media3 (ExoPlayer +
MediaSessionService), and Material 3 design.
# CONSTRAINTS
- 100% Kotlin + Jetpack Compose. No XML layouts.
- All network calls on Dispatchers.IO, wrapped in try/catch.
- Handle every failure gracefully: no-network, dead stream,
missing art, missing lyrics. The app must never crash on bad data.
და ყველაზე მნიშვნელოვანი ნაწილი — milestone-ებად დაყოფა, სადაც Claude თითო ეტაპის შემდეგ ჩერდება და დასტურს მთხოვს:
- Project scaffold — Gradle, dependencies, ვერსიებით
- Architecture — MVVM, single-activity, package structure
- Playback core —
MediaSessionService+ ExoPlayer + audio focus - Screen A — now-playing polling, album-art chain, lyrics
- Screen B + Favorites
- Home-screen widget (Glance)
გაკვეთილი: როცა AI-ს ეუბნები “გააკეთე ყველაფერი ერთბაშად”, იღებ ერთ უზარმაზარ, გადაუმოწმებელ კოდის ნაგავს. როცა ეუბნები “გააკეთე ნაბიჯი 1 და გაჩერდი” — იღებ კოდს, რომელსაც ნამდვილად კითხულობ და იგებ.
მუშაობის პროცესი

ტიპური session ასე გამოიყურებოდა: მარცხნივ — კოდის ხე და Claude Code-ის ცვლილებები (მწვანე/წითელი diff-ები terminal-ში), მარჯვნივ — ცოცხალი emulator, სადაც მაშინვე ვხედავ შედეგს.
Claude წერდა milestone-ს, მე ვუშვებდი emulator-ზე, ვუჩვენებდი რა მუშაობდა და რა — არა, და ვაგრძელებდით. ეს არ არის autocomplete — ეს დიალოგია.
Screen A — ვინილი, რომელიც ცოცხლობს

მთავარი ეკრანი ვინილის ფირფიტის სტილშია. ცენტრში — album art, რომელიც სიმღერის შეცვლისთანავე ავტომატურად ახლდება.
ყველაზე საინტერესო ნაწილი იყო album-art-ის მოძიების ჯაჭვი (fallback chain):
- ჯერ ვცდი art-ს თვითონ now-playing metadata-დან
- თუ იქ არ არის — ვეძებ გარე API-ში “შემსრულებელი + სათაური”-ით
- თუ არსად მოიძებნა — ვაჩვენებ bundled
cover.png-ს
ეს try → fallback → fallback ლოგიკა ზუსტად ის ადგილია, სადაც აპები ხშირად crash-ობენ. ამიტომ constraint-ში პირდაპირ ეწერა: “the app must never crash on bad data”.
// album art resolution — სამ საფეხურიანი fallback
val artUrl = nowPlaying.artworkUrl
?: artLookup.search(nowPlaying.artist, nowPlaying.title)
?: R.drawable.cover // bundled default
Lyrics — და “graceful” მარცხის ხელოვნება

Lyrics მოდის LRCLIB-დან — შემსრულებლისა და სათაურის მიხედვით. ხშირად ტექსტი ვერ მოიძებნება, და ეს ნორმალურია — მთავარია, აპი ამ დროს არ “გატყდეს”.
ამიტომ თითოეულ network call-ს აქვს მკაფიო state:
- ✅ მოიძებნა → ვაჩვენებ ტექსტს
- ⏳ იტვირთება → spinner
- ❌ ვერ მოიძებნა → “No lyrics available”, და არა ცარიელი თეთრი ეკრანი
ხარისხიანი აპი იქ არ ჩანს, სადაც ყველაფერი კარგად მიდის. ის იქ ჩანს, სადაც რაღაც გაფუჭდა — და აპმა მაინც იცის რა ქნას.
Screen B — მთელი მსოფლიოს რადიო

მეორე ეკრანი სრულიად სხვა ხასიათისაა — directory ათასობით სადგურით Radio Browser API-დან:
- 🔍 ძებნა debounce-ით (500ms — არ ვტენი server-ს ყოველ ასოზე)
- 🌍 ქვეყნის ფილტრი (მხოლოდ ის ქვეყნები, სადაც ≥5 სადგურია)
- 🏷️ ჟანრის chip-ები — multi-select
- ⭐ Favorites — ლოკალურად შენახული, count badge-ით
- 📄 “Load more” pagination
აქ განგებ არ არის album art ან lyrics — Radio Browser-ის სადგურებს ეს data-ი არ აქვთ. ამის constraint-ში ჩაწერა მნიშვნელოვანი იყო, თორემ AI “დაეხმარებოდა” და იქაც ალბომის ყდებს მოჰყვებოდა, სადაც არ უნდა.
რა გავაკეთეთ ჯამში
| ფენა | ტექნოლოგია |
|---|---|
| UI | Jetpack Compose, Material 3, Navigation Compose |
| Playback | Media3 ExoPlayer + MediaSessionService |
| Networking | Retrofit, OkHttp, kotlinx.serialization |
| Images | Coil 3 |
| Storage | Preferences DataStore |
| Widget | Jetpack Glance |
| ენა | 100% Kotlin |
პლუს ის, რასაც ვებ-ვერსია ვერასდროს მომცემდა:
- 🎵 background playback — ეკრანი ჩაქრა, მუსიკა უკრავს
- 🔒 lock-screen კონტროლი + headset/Bluetooth ღილაკები
- 📱 home-screen widget — album art, სათაური, Play/Pause, იგივე session-ზე მიბმული
დასკვნა: AI არ წერს აპს — ის წერს კოდს, შენ კი აპს
Claude Code-მ Radio Hangi-ის ათასობით ხაზი დაწერა. მაგრამ გადაწყვეტილებები ჩემი იყო: ორი ეკრანის გამიჯვნა, fallback-ების თანმიმდევრობა, “graceful failure”-ის წესი, milestone-ების რიგი.
თუ ერთ რამეს გამოვიტან ამ პროექტიდან:
კარგი prompt არ არის ბრძანება — ის არის სპეციფიკაცია. რაც უფრო ნათლად აღწერ რა და რატომ, მით უფრო ნაკლებს ასწორებ მერე.
კოდი ღიაა — შეგიძლია ნახო GitHub-ზე.
რა მოდის შემდეგ?
- Glance widget-ის ღრმა dive — როგორ ეკონტაქტება home-screen-ის ღილაკი იმავე
MediaSession-ს - Media3
MediaSessionService— audio focus და notification ნაბიჯ-ნაბიჯ - ვებ vs native — ერთი და იგივე რადიო, ორი მიდგომა, შედარება
გამოიწერე RSS — გაგრძელება მალე.
კითხვები ან იდეები? მომწერე — სიამოვნებით ვისაუბრებ Android-სა და AI-ით განვითარებაზე.