Flutter Deeper Course

Coming Soon

Back

How to force users to update your Flutter app

Having a force update mechanism in your app can save your life in critical situations, like when you've found a bug, fixed it and new version live on the store and you want users to only use the latest version.

I've developed versionarte to simplify this process. Using it you can force users to update your app, show optional update indicators and disable the app for maintenance with dynamic informative texts.

It gives you total freedom on UI, meaning that you can use your app's design system and keep its brand identity. That's what distinguishes it from other packages which generally give you a generic AlertDialog which you can only customize texts of.

Integration takes couple of minutes with its built-in Firebase Remote Config integration and also RESTful API endpoint option to control it directly from your backend.

Visual Examples

Here's how versionarte can enhance your app's update experience:

Forced Update Screen
Forced Update

Non-dismissible update screen for critical versions

Version Indicator
Version Indicator

Subtle notification for optional updates

Maintenance Mode
Maintenance Mode

Informative screen during app maintenance

These are just example implementations. With versionarte, you have complete freedom to design these screens according to your app's brand identity.

Features

Forced Update

What it is: You can force users to use the latest version of your app. Users on old version will see a page that they can't close.

When to use it:

  • Critical security vulnerabilities are found and fixed
  • Major API changes that make older versions incompatible
  • Breaking changes that could cause data loss or corruption

Real-world example: Imagine you've discovered a security vulnerability in version 2.1.0 that could expose user data. You've fixed it in version 2.1.1. Using forced update, you can ensure all users below 2.1.1 must update before continuing to use the app.

UX Tip

Use force updates strategically: only for critical security fixes, major API changes, or when older versions become incompatible. While it's good to keep users updated, frequent forced updates can lead to user frustration. A good practice is to force update every 1-2 months to ensure users aren't too far behind.

Optional Update

What it is: A gentle reminder to users that a new version is available, generally shown on home page as a non-intrusive indicator or an alert dialog.

When to use it:

  • New features are added
  • Non-critical bug fixes
  • Performance improvements
  • UI enhancements

Real-world example: You've added dark mode support in the latest version. While it's a great feature, it's not critical for app functionality. Users can choose to update at their convenience.

UX Tip

While showing new version availability via alert dialogs is common, be mindful of frequency. Showing it once per enough, otherwise when shown on every app launch it can be annoying.

Platform Independence

What it is: This is where versionarte truly stands out: you can control updates for Android and iOS apps separately, instead of forcing the same update rules for all platforms.

When to use it:

  • Platform-specific bugs need different minimum versions
  • Store approval timing differences between platforms
  • Different feature rollout schedules per platform

Real-world example: You've fixed an Android-specific crash in version 4.2.1, while iOS version 4.2.0 is stable and does not need to be updated. With platform independence, you can set:

  • Android minimum version: 4.2.1
  • iOS minimum version: 4.2.0 This way, only Android users are forced to update while iOS users can continue using their current version. This is also useful when you want to release a new feature only for Android users.

Maintenance Mode

What it is: A temporary app state that shows users a maintenance message instead of the regular app content.

When to use it:

  • Critical backend maintenance
  • Emergency situations requiring immediate app disable
  • Server migrations or updates
  • Investigating and fixing critical production issues

Real-world example: Your backend is undergoing a crucial database migration that will take 2 hours. Instead of users seeing failed API calls and errors, they'll see a friendly maintenance message that you can update in real-time with progress information.

Setup Guide

1. Installation

Add versionarte to your project:

dependencies:
  versionarte: <latest_version>

2. JSON Structure

Before diving into integration methods, let's understand the JSON structure that versionarte uses:

{
    "android": {
        "version": {
            "minimum": "2.7.0",  // Users below this version must update
            "latest": "2.8.0"    // Latest version on Play Store
        },
        "download_url": "https://play.google.com/store/apps/details?id=app.example",
        "status": {
            "active": true,     // Set to false for maintenance mode
            "message": {        // Shown during maintenance
                "en": "App is in maintenance mode, please come back later.",
                "es": "La aplicación está en modo de mantenimiento."
            }
        }
    },
    "iOS": {
        // Same structure as Android
    }
}

Think of it like a recipe:

  • If a user has version 2.6.0 (below minimum), they MUST update
  • If a user has version 2.7.0 (above minimum but below latest), they CAN update
  • If active is false, everyone sees the maintenance message
  • Messages can be in different languages (en, es, etc.) with 'en' as fallback
Note

This JSON structure is the same regardless of which integration method you choose. The only difference is where you store it (Firebase, your API, etc.).

3. Choose Your Integration Method

Option A: Firebase Remote Config (Recommended)

  1. Make sure Firebase is configured for your app
  2. Add this JSON to Remote Config with key "versionarte" (see Firebase Remote Config setup guide for detailed configuration steps)
  3. Start using this provider:
final VersionarteResult result = await Versionarte.check(
    versionarteProvider: RemoteConfigVersionarteProvider(),
);
Tip

If your app only integrates but does not initialize Firebase, you can set "initializeInternally: true" in the provider for versionarte to initialize it internally.

Note

You'll need to update the JSON in Firebase Console whenever you want to change version requirements or maintenance status.

Option B: Your Own API

  1. Create an endpoint that returns this JSON
Tip

You can integrate this JSON structure into your admin panel, making it easy to control app versions and maintenance mode directly from your own backend.

  1. Use the RESTful provider:
final VersionarteResult result = await Versionarte.check(
    versionarteProvider: RestfulVersionarteProvider(
        url: 'https://myapi.com/getVersioning',
    ),
);

Option C: Custom Provider

For other backends (GraphQL, Firestore, etc.), extend VersionarteProvider:

class MyCustomVersionarteProvider extends VersionarteProvider {
  @override
  Future<DistributionManifest> getDistributionManifest() async {
    // 1. Fetch your versioning data from anywhere
    final String result = await MyCustomService.fetchVersioning();
    
    // 2. Parse it into a Map
    final Map<String, dynamic> decodedResult = jsonDecode(result);
    
    // 3. Convert to DistributionManifest
    // This ensures your data follows the required structure
    return DistributionManifest.fromJson(decodedResult);
  }
}

Then use it as a provider:

final VersionarteResult result = await Versionarte.check(
    versionarteProvider: MyCustomVersionarteProvider(),
);
Note

Make sure your data follows the same JSON structure as shown above. The DistributionManifest.fromJson() method will validate your data format.

Usage Guide

1. Version Check Implementation

Performance Tip

Don't await Versionarte.check() at app startup - this can take 1 to 5 seconds depending on the provider and network. Run it in parallel to avoid delaying your app launch.

First, create a provider to handle version checking. Here's an example using Riverpod (you can adapt this to any state management solution):

final versionarteProvider = FutureProvider<VersionarteResult?>(
  (ref) => Versionarte.check(
    versionarteProvider: RemoteConfigVersionarteProvider(),
  ),
);

2. Handling Different States

It's recommended to implement the version check listener in your app's root widget (usually lib/app.dart where MaterialApp is defined). This ensures version and maintenance states are handled at the highest level of your app:

class App extends ConsumerWidget {
  const App({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Listen to version check results
    ref.listen(versionarteProvider, (previous, next) {
      if (next is AsyncData) {
        final VersionarteResult result = next.value!;
        
        if (result.status == VersionarteStatus.inactive) {
            // App is in maintenance mode
            showMaintenancePage(message: result.getMessageForLanguage('en'));
        } else if (result.status == VersionarteStatus.forcedUpdate) {
            // Force update required
            showForceUpdatePage();
        } else if (result.status == VersionarteStatus.outdated) {
            // New version available (optional update)
            // You can show an AlertDialog here, but we recommend
            // using a non-intrusive widget in your home page instead
            // (see VersionarteWidget example below)
        }
      }
    });
 
    return MaterialApp(
      ...
    );
  }
}

3. Building Update UI

Here's how to implement non-dismissible pages for different states:

void showForcedUpdatePage() {
  showGeneralDialog(
    context: context,
    barrierDismissible: false,
    pageBuilder: (_, __, ___) => PopScope(
      canPop: false,  // Prevents back button
      child: Scaffold(
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('New Version Available'),
                Text('Please update to continue'),
                ElevatedButton(
                  onPressed: () => Versionarte.launchDownloadUrl(result.downloadUrls),
                  child: Text('Update Now'),
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  );
}
 
void showMaintenancePage(String message) {
  showGeneralDialog(
    context: context,
    barrierDismissible: false,
    pageBuilder: (_, __, ___) => PopScope(
      canPop: false,
      child: Scaffold(
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.engineering_outlined, size: 48),
                Text('Maintenance'),
                Text(message),
              ],
            ),
          ),
        ),
      ),
    ),
  );
}

For optional updates, add this widget to your home page (example using Riverpod, adapt to your state management):

class VersionarteWidget extends ConsumerWidget {
  const VersionarteWidget({super.key});
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(versionarteProvider).when(
      data: (result) {
        if (result?.status == VersionarteStatus.outdated) {
          return GestureDetector(
            onTap: () => Versionarte.launchDownloadUrl(result!.downloadUrls),
            child: Container(
              padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
              decoration: BoxDecoration(
                color: Colors.teal.shade100,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(Icons.new_releases_outlined, size: 16),
                  SizedBox(width: 8),
                  Text('New version available'),
                ],
              ),
            ),
          );
        }
        return const SizedBox.shrink();
      },
      error: (_, __) => const SizedBox.shrink(),
      loading: () => const SizedBox.shrink(),
    );
  }
}

4. Store Integration

To open the appropriate store for the current platform:

// result is VersionarteResult object
final Map<TargetPlatform, String> downloadUrls = result.downloadUrls;
await Versionarte.launchDownloadUrl(downloadUrls);
Note

Don't hard code download URLs in your code. Always use the URLs from the VersionarteResult object. This is especially important because store URLs can change (for example, when transferring the app to another App Store Developer Account), and hard-coded URLs could lead users to non-existent pages.

What's Next?

For a detailed video walkthrough of versionarte's features and implementation, check out my upcoming video on YouTube.

If you encounter any issues or need help, feel free to open an issue on GitHub.


Kamran Bekirov

I'm Kamran Bekirov,

Flutter Developer who built 70+ mobile apps,

Founder of UserOrient - Flutter SDK for collecting user feedback,

Currently putting together a masterclass course at FlutterDeeper.