App Update Management
3 min read
Operations
| Installed version vs config | Action |
|---|---|
installed < min_supported_version | Force update — block app, send to store |
min_supported_version ≤ installed < latest_version | Optional — banner suggesting update |
installed ≥ latest_version | None |
You need two thresholds because "you must update" (security, breaking API change) is different from "we'd love it if you did" (new features, bug fixes).
Code in action
enum UpdateAction { none, optional, forced }
class AppUpdateService {
AppUpdateService(this._info, this._rc);
final PackageInfo _info;
final FirebaseRemoteConfig _rc;
Future<UpdateAction> check() async {
await _rc.fetchAndActivate();
final installed = Version.parse(_info.version);
final minSupported = Version.parse(_rc.getString('min_app_version'));
final latest = Version.parse(_rc.getString('latest_app_version'));
if (installed < minSupported) return UpdateAction.forced;
if (installed < latest) return UpdateAction.optional;
return UpdateAction.none;
}
}
class UpdateGate extends StatelessWidget {
const UpdateGate({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) => FutureBuilder<UpdateAction>(
future: context.read<AppUpdateService>().check(),
builder: (ctx, snap) => switch (snap.data) {
UpdateAction.forced => const ForceUpdateScreen(), // un-dismissible
UpdateAction.optional => Column(children: [
_OptionalBanner(onUpdate: () => _openStore()),
Expanded(child: child),
]),
_ => child,
},
);
}
UX patterns
| Type | UI | Dismiss |
|---|---|---|
| Forced | Full-screen modal with "Update" button | ❌ No way out |
| Optional | Top banner or onboarding card | ✅ Yes, with "remind me later" |
| In-app (Android) | Flexible (in-app download + restart) | ✅ User can defer |
| In-app immediate (Android) | Modal — must update | ❌ Same as forced |
For Android, look at in_app_update (Play Core wrapper). For iOS, you can't trigger updates programmatically — you can only deep-link to your App Store page.
Opening the store
Future<void> _openStore() async {
final url = Platform.isIOS
? Uri.parse('https://apps.apple.com/app/idYOUR_APP_ID')
: Uri.parse('https://play.google.com/store/apps/details?id=com.example.app');
await launchUrl(url, mode: LaunchMode.externalApplication);
}
Common mistakes to avoid
❌ Hard-coding "min version" in the binary
You ship a bad version → can't push a higher minimum.
✅ Read from Remote Config.
❌ Force-update without giving a clear reason
"Update required" with no context → users uninstall.
✅ Briefly explain why (security, critical fix).
❌ Force-updating right after launch with no rollback
New version breaks for 5% of users → those users are stuck.
✅ Gradual rollout; pause if Crashlytics spikes.
❌ Checking for updates on every screen
Wastes Remote Config quota; jittery banners.
✅ Check once on app start (or once per session).
❌ Showing the optional update banner repeatedly with no snooze
Users dismiss → next launch shows again → annoying.
✅ Remember "snoozed until" in SharedPreferences.
❌ Not handling the store-not-installed case
Sideloaded / enterprise builds → launchUrl fails.
✅ Fallback message with manual instructions.
Interview follow-ups
-
Why use TWO version thresholds (min_supported + latest)? They serve different purposes.
min_supportedis for breaking changes (server API removed an endpoint, security fix mandatory) — these must block usage.latestis for recommended updates — better UX, new features. Conflating them either spam-updates users or fails to gate critical issues. -
How do you handle a force-update that locks users out of their data? Make sure the update flow is bulletproof: clear copy, one-tap to store, store deep link works. Test before raising the minimum version. Have a remote kill switch: if Crashlytics spikes after a forced update, roll back the
min_supported_versionin Remote Config and the lock disappears. -
What's different about Android's in-app updates? Play Core lets you trigger an in-app update flow: the user stays in your app while it downloads, then restarts. Two modes: Flexible (deferred restart, user can keep using the old version) and Immediate (must update now). iOS has no equivalent — you can only deep-link to the store page.
-
How do you test the update flow? In a staging build, override Remote Config values to force the "forced" or "optional" state. Verify the screens look right and the store deep links work. For real updates, use TestFlight (iOS) or Play internal testing track — those let you actually trigger an in-app update from one build to another.
How helpful was this content?
Please sign in to rate this article.