Flutter Hotfixes in Production: Strategies for Instant Bug Patches on iOS and Android
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
It’s every mobile developer’s nightmare: you ship a new release, it passes review, and within hours you see a critical bug in production—maybe a crash on a specific device or OS version. On Android, you can push a fix relatively quickly. On iOS, you’re at the mercy of the App Store review, which can take a day or more. In the meantime, users are having a broken experience, and your team is scrambling.
The traditional “fix, resubmit, wait” cycle is simply unacceptable for mission-critical issues. Fortunately, as Flutter developers, we have several strategies to deliver instant (or near-instant) patches and mitigate user impact. Let’s explore the most practical approaches.
1. Server-Side Feature Flags: Your First Line of Defense
The most robust way to prevent a bug from affecting all users is to control feature availability remotely. By wrapping new features in a server-side toggle, you can disable a problematic feature instantly, without any app update.
How it works: Your app fetches a configuration (e.g., from Firebase Remote Config, your own API, or a service like LaunchDarkly) on startup or at runtime. This configuration determines whether a specific feature is enabled.
Example using Firebase Remote Config: First, wrap your feature in a conditional check:
import 'package:firebase_remote_config/firebase_remote_config.dart';
class FeatureFlagService {
static final RemoteConfig _remoteConfig = RemoteConfig.instance;
static Future<void> initialize() async {
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(seconds: 30),
));
await _remoteConfig.fetchAndActivate();
}
static bool isFeatureEnabled(String featureKey) {
return _remoteConfig.getBool(featureKey);
}
}
// In your widget or service:
void loadFeature() async {
await FeatureFlagService.initialize();
if (FeatureFlagService.isFeatureEnabled('new_chat_ui')) {
// Launch the new, potentially buggy feature
Navigator.push(context, MaterialPageRoute(builder: (_) => NewChatUI()));
} else {
// Fall back to the stable, old UI
Navigator.push(context, MaterialPageRoute(builder: (_) => LegacyChatUI()));
}
}
On your Firebase Remote Config console, you can set the key new_chat_ui to false and publish changes. Within minutes, all users fetching the updated config will be routed away from the buggy screen. This gives you breathing room to develop and test a proper fix.
Common mistake: Not building a sensible fallback. Always design your flags so that disabling a feature reverts users to a stable, functional state.
2. Code Push with Shorebird: Instant Over-the-Air Updates
While server-side flags are great for toggling features, they can’t patch logic bugs inside your already-shipped Dart code. For that, you need a code push solution. In the Flutter ecosystem, Shorebird is the leading tool that allows you to push updates to your Dart code and assets directly to users’ devices, bypassing the store review process for both iOS and Android.
How it works: Shorebird creates a separate code push system for your Flutter app. You build your app with their tools, and they provide a channel to deliver patches.
Basic integration:
import 'package:shorebird_code_push/shorebird_code_push.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _shorebirdCodePush = ShorebirdCodePush();
@override
void initState() {
super.initState();
_checkForUpdates();
}
Future<void> _checkForUpdates() async {
// Check if a new patch is available.
final isUpdateAvailable = await _shorebirdCodePush.isNewPatchAvailableForDownload();
if (isUpdateAvailable) {
// Download the new patch (it will be used on the next app restart).
await _shorebirdCodePush.downloadUpdateIfAvailable();
// In a real app, you might prompt the user to restart.
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Shorebird Code Push Demo')),
body: Center(
child: Text('Current patch number: ${_shorebirdCodePush.currentPatchNumber()}'),
),
),
);
}
}
To deploy a hotfix, you’d use the Shorebird CLI:
shorebird patch android
shorebird patch ios
Important Caveats:
- Apple’s Policies: While Shorebird works technically on iOS, Apple’s guidelines state that apps must not download executable code. Shorebird is designed to comply by only updating code written in Dart (which they interpret as “scripting”), but it’s crucial to understand the policy risk. Use it for critical bug fixes, not for wholesale feature changes.
- Binary Changes: Code push cannot modify native code (Swift, Kotlin, Objective-C, etc.) or plugin implementations. It’s for Dart code and assets only.
3. The Nuclear Option: Expedited App Store Review
When a bug is severe (causing crashes, data loss, or security issues) and you have no other mitigation, you can request an expedited review from Apple. This is not a tool for routine fixes but a genuine emergency channel. In the request, you must clearly explain the severity of the bug and its impact on users. If granted, reviews can happen in a matter of hours.
Putting It All Together: A Layered Strategy
The most resilient production strategy uses a combination of these tools:
- Wrap all new features in server-side flags. This is cheap insurance and allows for controlled rollouts and instant kill-switches.
- Integrate a code push solution like Shorebird for your codebase. Use it to deploy low-risk, critical bug fixes for logic errors in your Dart code.
- Reserve expedited reviews for true emergencies involving native code or when other methods aren’t viable.
By adopting this layered approach, you move from a reactive posture—hoping nothing slips through—to a proactive one where you have multiple levers to pull to protect your users’ experience the moment an issue is detected.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations
Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.
Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues
Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.