Mastering In-App Updates in Flutter: A Cross-Platform Solution for Android and iOS
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Keeping your Flutter app up-to-date is crucial for delivering bug fixes, new features, and security patches. However, implementing a smooth update experience can be surprisingly fragmented. On Android, you have a powerful native In-App Update API. On iOS, you have… well, you’re directed to the App Store. Managing two completely different code paths for this core user flow is a hassle. What if you could handle it all with a single, unified logic in your Flutter code?
Let’s build a cross-platform solution that provides the best possible update experience on each platform, without duplicating your code.
The Platform Divide
The core of the problem lies in the platform-specific capabilities:
- Android: Offers a flexible
InAppUpdateManagerAPI. It supports two modes: a flexible update that lets the user continue using the app while downloading, and an immediate update that requires a restart. This happens entirely within your app. - iOS: Apple does not provide a direct equivalent API. The only official method to prompt for an update is to direct the user to your app’s page on the App Store, which requires leaving your application.
Our goal is to abstract this complexity. We’ll create a Dart interface that our Flutter widgets can call, and then implement platform-specific logic behind the scenes.
Building the Unified Interface
First, we define a contract. This AppUpdater class will be the single point of contact for our UI.
abstract class AppUpdater {
/// Checks for an update and returns `true` if one is available.
Future<bool> checkForUpdate();
/// Performs the platform-appropriate update flow.
/// [immediate] is a hint for Android (Flexible vs Immediate mode).
Future<void> performUpdate({bool immediate = false});
/// A stream to listen for download progress (primarily for Android Flexible).
Stream<double>? get updateProgress;
}
Implementing the Android Solution
For Android, we use the official in_app_update plugin to access the native APIs. We create a concrete implementation, AndroidAppUpdater.
Key Steps:
- Check update availability using
InAppUpdate.checkForUpdate(). - For an immediate update, call
InAppUpdate.performImmediateUpdate(). - For a flexible update, call
InAppUpdate.startFlexibleUpdate()and listen to the download stream, then complete the update withInAppUpdate.completeFlexibleUpdate().
import 'package:in_app_update/in_app_update.dart';
class AndroidAppUpdater implements AppUpdater {
@override
Future<bool> checkForUpdate() async {
try {
final info = await InAppUpdate.checkForUpdate();
return info.updateAvailability == UpdateAvailability.updateAvailable;
} catch (e) {
// Handle error, perhaps assume no update
return false;
}
}
@override
Future<void> performUpdate({bool immediate = false}) async {
if (immediate) {
await InAppUpdate.performImmediateUpdate();
} else {
// Flexible Update Flow
await InAppUpdate.startFlexibleUpdate();
// In a real scenario, you would listen to `updateProgress`
// and show a UI. Once done:
await InAppUpdate.completeFlexibleUpdate();
}
}
@override
Stream<double>? get updateProgress => null; // Could map from plugin events
}
Common Android Mistake: Forgetting to call completeFlexibleUpdate() after the download finishes. The app will restart on its own, but without this call, the update won’t be applied until the next restart, confusing users.
Implementing the iOS Solution
For iOS, we use the url_launcher plugin to open the App Store URL for our app. The implementation is simpler but requires the app’s unique App Store ID.
import 'package:url_launcher/url_launcher.dart';
class IOSAppUpdater implements AppUpdater {
// You must provide your app's App Store ID.
static const String appStoreId = 'YOUR_IOS_APP_ID';
static const String appStoreUrl = 'https://apps.apple.com/app/id$appStoreId';
@override
Future<bool> checkForUpdate() async {
// iOS has no official API to check. A common workaround is to compare
// the local version with a version fetched from your own backend.
// For simplicity here, we'll assume an update is always "available"
// and let the App Store page be the final authority.
return true;
}
@override
Future<void> performUpdate({bool immediate = false}) async {
final uri = Uri.parse(appStoreUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
throw Exception('Could not launch App Store.');
}
}
@override
Stream<double>? get updateProgress => null; // Not applicable for iOS
}
Important iOS Consideration: The checkForUpdate method is a stub. In a production app, you would typically implement a version check against an endpoint you control (e.g., a simple JSON file on your server containing the latest published version number). This avoids unnecessarily sending users to the App Store if they are already on the latest version.
Tying It All Together in Flutter
Now, we use the power of conditional imports to create a platform-aware factory. Create a file named app_updater.dart.
import 'dart:io';
// This is the main file your widgets will import.
AppUpdater getAppUpdater() {
// Conditional import based on the platform
if (Platform.isAndroid) {
return AndroidAppUpdater();
} else if (Platform.isIOS) {
return IOSAppUpdater();
}
throw UnsupportedError('Platform not supported');
}
Finally, in your UI—perhaps a settings screen or a mandatory update dialog—you can use this seamlessly.
ElevatedButton(
onPressed: () async {
final updater = getAppUpdater();
final hasUpdate = await updater.checkForUpdate();
if (hasUpdate) {
// Show a dialog explaining the update
await updater.performUpdate(immediate: false);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('You are on the latest version!')),
);
}
},
child: const Text('Check for Updates'),
),
Final Thoughts
This pattern keeps your business logic clean and consistent. Your Flutter code asks for an update, and the right thing happens on each platform: a native Android experience or a trip to the iOS App Store. Remember to handle errors gracefully, provide clear UI feedback during the Android flexible download, and implement a proper version check service for iOS to complete the solution.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease
Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.
Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows
Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.
Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows
Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.