After a year of nights and weekends, I shipped Lucidcast — an
Android podcast player I built because every mainstream app I tried
was missing the same three things. The app went live on Google Play
this week. This post is for indie devs thinking about shipping
their own Android project — six lessons I learned the expensive way.
What Lucidcast does (90-second context)
A podcast player with on-device AI:
- Whisper transcribes downloaded episodes locally (audio never leaves the phone)
- AI episode summaries via Gemini, with a live progress ring
- "Podcast" wake-word voice commands that work on the lock screen and in Android Auto
- Smart Pause auto-pauses on loud noises or when someone talks to you
Plus the usual: chapters, transcripts, value tags (Podcasting 2.0),
22 languages, Android Auto, live radio, no accounts, no ads.
Free includes the full podcast player. Pro one-time unlocks the
audio intelligence engine. AI Pack subscription enables Whisper +
summaries. Made in EU by a one-person Czech indie team
(Prismatic s.r.o.).
OK, lessons.
1. On-device Whisper is harder than it looks (NDK 29 patch needed)
I wanted local transcription so audio doesn't leave the device.
whisper_ggml is the only Flutter binding I found that works
end-to-end. Catch: it needs Android NDK 29.0.13113456 while
most other plugins are still on 27. Setting ndkVersion = "29..."
in android/app/build.gradle.kts works for new builds, but the
plugin's auto-detected version was sometimes off — needed a small
patch script (flutter/tool/patch_whisper_ggml.sh) to enforce it
during CI.
Battery cost is real: I gate transcription to charging-only mode
by default with a configurable battery threshold (10-80 %, default
50 %). When the user toggles "transcribe downloaded episodes," I
queue the work but only execute when the phone is plugged in.
Users on r/podcasts would otherwise notice a 5-10 % overnight
battery drain.
2. Sharing the microphone is a contract nobody documents
Smart Pause uses noise_meter to read ambient dB. Voice commands
use Android's native SpeechRecognizer. Both want exclusive access
to MediaRecorder.AudioSource.MIC, and if you don't coordinate
them explicitly, the second consumer gets silent samples and every
recognition cycle ends in ERROR_NO_MATCH.
The fix: a centralised NoiseDetectionService with a block() /
unblock() protocol. When the voice command service starts, it
calls block('voice_commands'), which cancels the noise meter's
subscription and refuses any auto-restart. On stop, unblock()
optionally restarts capture for the noise-dependent consumers.
There's a 200 ms grace window between block and SpeechRecognizer. so the platform finishes tearing down the previous
startListening()
AudioRecord. Without that grace window, the bug returns
intermittently.
3. R8 resource shrinking + drawables-loaded-by-name = silent regressions
This one cost me an entire release cycle. The audio_service
plugin lets you wire MediaControl.custom actions with
androidIcon: 'drawable/ic_save_thought_aa'. At runtime, native
code does Resources.getIdentifier() to resolve the name — a
dynamic lookup, invisible to R8.
In debug builds, every drawable ships, so it works. In release
builds with isShrinkResources = true, R8 sees no static reference
to ic_save_thought_aa.xml, prunes it, and at runtime
createCustomAction throws IllegalArgumentException. That
exception aborts the entire setState() call. The MediaSession
stays at state=NONE, the system never renders the playback
notification or lock-screen controls, and Google Play eventually
flags the app for not working in Android Auto.
Fix: add a res/raw/keep.xml with
tools:keep="@drawable/ic_save_thought_aa". Five lines. Caught
because I had S25 + Edge running production-parity dumpsys
comparisons after every release build.
4. The Google Play reviewer tests every tier of your paywall
I had a Pro-only tier gate on the Android Auto browse tree —
canBrowseCatalog() returned false for free-tier users, so
getChildren() short-circuited to an empty list. Reviewer (always
free-tier, always fresh install) opened the app in Android Auto,
saw "No items," and rejected the release with "App doesn't perform
as expected — does not load any content after using the stop button."
Lesson: anything Google's automated test rig can reach must work
for free-tier users at the basic functional level. Paywalls
belong on premium features (AI summary, voice commands), not on
hygiene features (browse, play, stop). I removed the gate entirely
and let entity-level limits (max 3 subscriptions on free) do the
differentiation organically.
5. Falling back to a 6 G heap on a 7.5 G dev box is not optional
My dev server had 7.5 GB RAM. Gradle's default -Xmx8G plus
Kotlin compiler daemon's own -Xmx8G plus the Flutter tool plus
system overhead crashed mid-build with "Gradle build daemon
disappeared unexpectedly" once swap saturated.
Workaround: drop org.gradle.jvmargs heap to -Xmx6G and
MaxMetaspaceSize=2G. Builds are 30 % slower but they finish.
Long-term fix: bump the dev server RAM (which I eventually did).
If you're on a 16 GB laptop, you're probably fine. If you're on
an 8 GB VM, profile first.
6. Localisation is a Gemini batch script away
The app ships in 22 languages. I wrote flutter/tool/translate_arbs.py
that takes app_en.arb and produces 20 other ARBs via Gemini 2.5
Flash. Two-pass approach: pass 1 fills missing keys, pass 2
re-translates keys whose value still matches the English source
(catches the "I forgot to localise this dialog" case after the
fact). Heuristic skips short (<5 char) and uppercase-only strings
so brand names and unit labels stay intact.
Total cost for 22 languages: under $2 per re-translation run.
Quality is good enough that I've only had to manually correct
two locales after user feedback.
Where I'm at now
- Live on Google Play, v1.1.0+248
- ~30 published reviews on the previous build cycle, fresh review reset on the new version
- Solo founder, no investors, EU privacy-first positioning
- Looking for honest feedback from indie devs and Android power-users
If you've shipped an indie Android app in the past two years, I'd
love to hear what your post-launch first-30-days playbook was.
Discovery on Play Store is the part I'm finding hardest — no
ASO budget, no influencer relationships, no paid acquisition until
the listing has 50+ reviews to convert.
Early tester program (DEV.to readers)
If you're going to actually use Lucidcast, I'm giving out 20 free
90-day AI Pack trial codes to DEV.to readers. Drop me a DM if you
want one.
The honest mechanics, because nobody likes surprises:
- The code gives you 90 days of AI Pack (on-device Whisper + AI summaries) for free
- After 90 days, the subscription auto-renews at the normal price (~€3/month) unless you cancel — Google Play lets you cancel any time from Play Store → Subscriptions
- Why 90 days, not lifetime: Google Play caps subscription promo codes at 90 days. If you want a fresh 90 days when this runs out, DM me — happy to send another code as long as you've been using the app and willing to share occasional feedback
To be explicit because Play Store policy matters: the code is for
feedback to me directly, not for a Play Store review. I can't tie
the two for compliance reasons and won't ask you to. If you happen
to leave a review separately because you want to, that's your call,
not part of the deal.
How it works:
- DM me the Google account email you use for Play Store
- I generate a promo code and send it back
- You redeem it in the app or via Play Store; AI Pack unlocks for 90 days
- After ~1 week of use, send me a short DM with what worked, what didn't, what's missing
- When 90 days runs out, either let it auto-renew (you decided you like it), cancel, or ping me for a fresh code
First 20 takers. Czech indie can't comp the world but can comp the
early DEV.to crowd.
Thanks for reading. Comments and bug reports are open.













