Recently, a client messaged me:
"Hey, I don't see the new features. Are you sure it updated?"
It turns out they hadn't updated the app at all. Sound familiar? We've all been there. You ship a build, announce the fix, and then spend two days answering messages from people running the old version. Update adoption is one of those problems nobody budgets for because it doesn't feel like a feature — until it's eating your support time and your client's trust.
Here's what changed everything for me, on both platforms.
Android: Google's In-App Update API
The magic came from Google's own In-App Update API (Play Core). Suddenly, users got notified instantly whenever I pushed a new version through Play's internal test track. No more guessing games or manual APK juggling — the app prompts them to update right where they are.
In Flutter, the in_app_update package wraps it. The mechanics worth knowing:
- Flexible updates download in the background while the user keeps using the app, then apply when convenient. This is the right default for feature releases — it's polite.
- Immediate updates take over with a full-screen flow and won't let the user continue on the old version. I reserve these for genuinely breaking releases: API contract changes, critical fixes, security issues.
The pattern I ship: check for an available update on app start, run flexible by default, and keep a server-controlled flag (remote config works fine) that can escalate any release to immediate without shipping anything. When an old client version becomes dangerous, flipping that flag is a one-minute operation instead of an emergency release.
One caveat that catches people: in-app updates only work for builds installed from Play — not sideloaded APKs, not debug builds. Test the flow through the internal testing track, because that's the only place it's real.
iOS: Appcast RSS feeds
But I hit another snag. Apple's TestFlight doesn't integrate neatly with public App Store lookups, which is another wall — during heavy testing phases, iOS testers were exactly the people stuck on stale builds without knowing it.
The solution? Appcast RSS feeds.
An appcast is just a small RSS/XML file describing your latest version — the same mechanism desktop apps have used for years. By hosting my own feed (GitHub Pages is perfect for this: free, versioned, reliable), the Flutter upgrader package instantly recognized new builds and showed testers a clear "a newer version is available" prompt. Update the XML when you cut a build — that's the entire maintenance burden. Testers finally saw updates clearly — and my sanity was restored.
For production iOS builds, upgrader checks the App Store listing directly, so the same in-app prompt covers the store release path too — the appcast simply fills the gap the store lookup can't reach.
The part that's actually product design
Once the mechanics work, the remaining decisions are about tone:
- Don't prompt on every launch. Nagging trains users to dismiss the dialog reflexively. Prompt once per version, with a snooze.
- Say what's in it. "Update available" converts worse than one line about what improves. Users update for reasons, not for version numbers.
- Force rarely, but decisively. A minimum-supported-version gate (again, server-controlled) is kinder than letting someone run a build that silently fails against your new API.
The takeaway
Ever found yourself stuck in the same loop of update confusion? These two approaches were absolute game-changers. Small infrastructure decisions like these are often the difference between a smooth launch and a week of "did it even update?" messages — and they're a half-day of work, once, per app.
The build you shipped only matters if people are actually running it.