← Back to posts Cover image for Mastering In-App Updates in Flutter: A Guide to Seamless User Experience on iOS & Android

Mastering In-App Updates in Flutter: A Guide to Seamless User Experience on iOS & Android

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Keeping users on the latest version of your Flutter app isn’t just about shipping new features—it’s critical for security patches, bug fixes, and ensuring a consistent experience. Traditionally, prompting an update meant sending users to the app store, breaking their flow and potentially causing them to abandon the process altogether. The solution? In-app updates.

This guide walks through implementing a unified in-app update system for both iOS and Android, creating a seamless experience that keeps users up-to-date without ever leaving your app.

The Core Problem: Platform Fragmentation

The challenge stems from a fundamental platform difference. Android has long offered a native AppUpdateManager API for flexible in-app updates. iOS, however, lacked a direct equivalent, forcing developers to use a clunky workaround: show an alert and then programmatically open the App Store URL, which closes the current app.

This disparity meant Flutter developers either had to settle for a subpar, disruptive experience on iOS or maintain two completely different update flows. We need a single, elegant solution.

The Unified Solution: in_app_update

The in_app_update package bridges this gap, providing a consistent API that works gracefully on both platforms. Under the hood, it uses the native Android APIs for a truly in-app experience and implements a sleek, iOS-optimized flow that minimizes disruption.

Let’s get it set up.

1. Add the Dependency Add the package to your pubspec.yaml:

dependencies:
  in_app_update: ^2.0.0

Run flutter pub get.

2. Platform-Specific Configuration

Android: Ensure your app is published on Google Play. The API requires it. No extra permissions are needed.

iOS: You need to fetch your app’s ID from App Store Connect. Add a key to your app’s Info.plist (located at ios/Runner/Info.plist):

<key>AppStoreId</key>
<string>YOUR_APP_STORE_ID_NUMBER</string>

Replace YOUR_APP_STORE_ID_NUMBER with the numeric ID of your app (e.g., 1234567890).

Implementing the Update Flow

The logic follows three clear steps: check for an update, see if it’s flexible or immediate (Android), and then present the appropriate prompt to the user. Here’s a practical implementation you can drop into your app.

Let’s create a simple service class to handle this:

import 'package:in_app_update/in_app_update.dart';

class AppUpdateService {
  static Future<void> checkForUpdate() async {
    try {
      // 1. Check for update availability
      final updateAvailability = await InAppUpdate.checkForUpdate();
      
      if (!updateAvailability.updateAvailable) {
        print('App is up to date.');
        return;
      }

      // 2. (Android) Check update type - Flexible or Immediate
      //    For iOS, this will always be [AppUpdateType.flexible].
      final updateType = updateAvailability.updateType;

      // 3. Trigger the update flow
      if (updateType == AppUpdateType.immediate) {
        // For critical updates, the system handles the full-screen UI.
        await InAppUpdate.performImmediateUpdate();
      } else {
        // For flexible updates, we control the experience.
        await _handleFlexibleUpdate();
      }
    } catch (e) {
      print('In-app update failed: $e');
      // Fallback: You could log this error or inform the user.
    }
  }

  static Future<void> _handleFlexibleUpdate() async {
    // Start the flexible update download in the background.
    final updateResult = await InAppUpdate.startFlexibleUpdate();

    if (updateResult == UpdateResult.success) {
      // The update is downloaded. Now prompt the user to install.
      await InAppUpdate.completeFlexibleUpdate();
      // On Android, the app will restart. On iOS, the App Store will open.
    } else {
      print('Flexible update failed or was canceled.');
    }
  }
}

To trigger this check, you might call it during app startup or from a settings screen:

// In your initState or a button onTap
void _checkUpdate() {
  AppUpdateService.checkForUpdate();
}

Designing a User-Friendly Prompt

Blindly triggering the update check on startup can be jarring. A better pattern is to check in the background and then show a custom, non-blocking UI prompt if a flexible update is available.

// Example using a simple dialog
Future<void> _checkAndPromptUpdate(BuildContext context) async {
  final updateAvailability = await InAppUpdate.checkForUpdate();
  
  if (updateAvailability.updateAvailable &&
      updateAvailability.updateType == AppUpdateType.flexible) {
    // ignore: use_build_context_synchronously
    showDialog(
      context: context,
      barrierDismissible: false, // User must decide
      builder: (ctx) => AlertDialog(
        title: Text('Update Available'),
        content: Text('A new version of the app is ready to download. Would you like to install it now?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: Text('Later'),
          ),
          TextButton(
            onPressed: () async {
              Navigator.pop(ctx);
              await AppUpdateService._handleFlexibleUpdate();
            },
            child: Text('Install Now'),
          ),
        ],
      ),
    );
  }
}

Common Pitfalls & Best Practices

  1. Don’t Block the Main Thread: Always wrap your update calls in try-catch blocks and avoid calling performImmediateUpdate without user context on Android, as it takes over the screen.
  2. Respect User Choice: For flexible updates, always let the user decide when to install. Provide a “Later” option.
  3. Test Thoroughly on Both Platforms: The behavior differs. On iOS, completeFlexibleUpdate() will open the external App Store. On Android, it restarts the app internally.
  4. Handle Errors Gracefully: Network issues or store API errors can occur. Your app should not crash if the update check fails.
  5. Use a Sensitive Trigger: Consider checking for updates after a user achieves a natural milestone (completing a task, closing a session) rather than immediately on launch.

By integrating in-app updates, you move from a disruptive, platform-dependent chore to a polished part of your app’s maintenance routine. Your users get the latest improvements with minimal friction, and you gain confidence that critical updates are actually being installed. It’s a win-win that feels native on both sides of the mobile ecosystem.

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.