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
Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations
Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.
Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity
Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.
Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps
Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.