← Back to posts Cover image for Mastering In-App Updates in Flutter: A Cross-Platform Solution for Android and iOS

Mastering In-App Updates in Flutter: A Cross-Platform Solution for Android and iOS

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

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 InAppUpdateManager API. 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:

  1. Check update availability using InAppUpdate.checkForUpdate().
  2. For an immediate update, call InAppUpdate.performImmediateUpdate().
  3. For a flexible update, call InAppUpdate.startFlexibleUpdate() and listen to the download stream, then complete the update with InAppUpdate.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

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

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.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

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.