Introduction
I recently added a Force Upgrade Dialog to my flutter app called Mellotippet, and I’d like to share why and how I did it. The source code can be found here: https://github.com/molundb/mellotippet.
Why Force Upgrade is important
There are several good reasons to have a force upgrade mechanism in place:
- The backend can retire API versions and move faster, since they need to support fewer versions.
- New security updates can be enforced.
- If a severe bug is detected, it can be ensured that users upgrade to the hotfix version.
It’s important to add force upgrade capabilities as early as possible, since you won’t be able to force users before that point to upgrade their version.
In this blog post, we’ll explore how to add a force upgrade dialog to a Flutter app using Firebase Remote Config. This approach allows you to prompt users to upgrade to the latest version directly from your app.
Prerequisites
Before we begin, make sure you have the following set up:
- A Flutter project.
- A Firebase project with Remote Config enabled.
Step 1: Set up Firebase Remote Config
Define a Remote Config parameter for the minimum app version. In the Firebase console, add a parameter named requiredMinimumVersion
. If you also want to be able to recommend users to upgrade to a version without forcing them, add recommendedMinimumVersion
.
Step 2: Fetch values from Firebase Remote Config
-
Install Dependencies:
Open your
pubspec.yaml
file and add a dependency on firebase_remote_config.dependencies: firebase_remote_config: ^X.X.X // Replace X.X.X with the latest version of the firebase_remote_config flutter package.
-
Initialize Firebase:
In your
main.dart
file, initialize Firebase in themain()
function:import 'package:firebase_core/firebase_core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); ... }
-
Initialize Firebase Remote Config:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); await MyFirebaseRemoteConfig.initialize(); ... }
import 'package:firebase_remote_config/firebase_remote_config.dart'; class MyFirebaseRemoteConfig { final remoteConfig = FirebaseRemoteConfig.instance; static Future<void> initialize() async { final remoteConfig = FirebaseRemoteConfig.instance; await remoteConfig.setConfigSettings( RemoteConfigSettings( fetchTimeout: const Duration(minutes: 1), minimumFetchInterval: const Duration(hours: 1), ), ); // These will be used before the values are fetched from Firebase Remote Config. await remoteConfig.setDefaults(const { 'requiredMinimumVersion': '4.0.0', 'recommendedMinimumVersion': '4.0.0', }); // Fetch the values from Firebase Remote Config await remoteConfig.fetchAndActivate(); // Optional: listen for and activate changes to the Firebase Remote Config values remoteConfig.onConfigUpdated.listen((event) async { await remoteConfig.activate(); }); } // Helper methods to simplify using the values in other parts of the code String getRequiredMinimumVersion() => remoteConfig.getString('requiredMinimumVersion'); String getRecommendedMinimumVersion() => remoteConfig.getString('recommendedMinimumVersion'); }
Step 3: Get the user’s current app version
To be able to know if the user has an outdated version, we need to know their current version. To achieve this, we’ll use package_info_plus.
-
Install Dependencies:
Open your
pubspec.yaml
file and add a dependency on package_info_plus.dependencies: package_info_plus: ^X.X.X // Replace X.X.X with the latest version of the package_info_plus flutter package.
-
Create a class to get and store the app’s version:
import 'package:package_info_plus/package_info_plus.dart'; class MellotippetPackageInfo { late String version; Future<void> initialize() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); version = packageInfo.version; } }
-
Intialize:
Call
initialize()
inmain.dart
. In this example we use get_it as a service locator, but you can use whatever you like. The important thing is that the version is fetched and can be used in the comparison in the next step.import 'package:firebase_core/firebase_core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); await getIt.get<MellotippetPackageInfo>().initialize(); ... }
Step 4: Define Force Upgrade Logic
Now that we have both the Remote Config version and the user’s version, we can compare them to determine if we should show an upgrade dialog.
-
Check if the dialog should be shown
Create a stateful widget called ForceUpgradePage that displays a dialog if a force upgrade is required.
class ForceUpgradePage extends StatefulWidget { const ForceUpgradePage({super.key}); @override State<ForceUpgradePage> createState() => _ForceUpgradeState(); } class _ForceUpgradeState extends State<ForceUpgradePage> { // Get the necessary classes using get_it final packageInfo = getIt.get<MellotippetPackageInfo>(); final featureFlagRepository = getIt.get<FeatureFlagRepository>(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { // Get the current app version var appVersion = _getExtendedVersionNumber(packageInfo.version); // Get the required min version from Firebase Remote Config var requiredMinVersion = _getExtendedVersionNumber( featureFlagRepository.getRequiredMinimumVersion()); // Get the recommended min version from Firebase Remote Config var recommendedMinVersion = _getExtendedVersionNumber( featureFlagRepository.getRecommendedMinimumVersion()); // Compare the versions and display a dialog if the app version is lower than // the required or recommended version if (appVersion < requiredMinVersion) { _showUpdateVersionDialog(context, false); } else if (appVersion < recommendedMinVersion) { _showUpdateVersionDialog(context, true); } else { // If the current version is higher than the required and recommended version, navgiate to the next Page - in this case the LoginPage() Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => const LoginPage(), ), ); } }); } @override Widget build(BuildContext context) { return Scaffold( body: Container(), ); } ... // Helper method to compare two semver versions. int _getExtendedVersionNumber(String version) { List versionCells = version.split('.'); versionCells = versionCells.map((i) => int.parse(i)).toList(); return versionCells[0] _ 100000 + versionCells[1] _ 1000 + versionCells[2]; } }
-
Display the force upgrade dialog:
Show the force upgrade dialog if the app version is lower than either the required or recommended min version.
Future<void> _showUpdateVersionDialog(BuildContext context, bool isSkippable) async { return showDialog<void>( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text("New version available"), content: SingleChildScrollView( child: ListBody( children: <Widget>[ Text("Please update to the latest version of the app."), ], ), ), actions: <Widget>[ // A "skip" button is only shown if it's a recommended upgrade isSkippable ? TextButton( child: const Text('Skip'), onPressed: () { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => const LoginPage(), ), ); }, ) : Container(), TextButton( child: const Text('Update'), onPressed: () { _launchAppOrPlayStore() }, ), ], ); }, ); }
Step 5 (optional): Open App Store / Google Play Store
To make it easy for users to update their apps, make the “Update” button launch the App store on iOS devices and Google Play Store on Android devices.
-
Install Dependencies:
Open your
pubspec.yaml
file and add a dependency on url_launcher.dependencies: url_launcher: ^X.X.X // Replace X.X.X with the latest version of the url_launcher flutter package.
-
Launch App/Play store
Launch the App store on iOS devices and Google Play Store on Android devices.
void _launchAppOrPlayStore() { final appId = Platform.isAndroid ? 'YOUR_ANDROID_PACKAGE_ID' : 'YOUR_IOS_APP_ID'; final url = Uri.parse( Platform.isAndroid ? "market://details?id=$appId" : "https://apps.apple.com/app/id$appId", ); launchUrl( url, mode: LaunchMode.externalApplication, ); }
Step 6: Use ForceUpgradePage
Decide when you want to display the force upgrade dialog. Usually this is done as one of the first things in the app. In my case I set ForceUpgradeApp as the widget of the home parameter of my MaterialApp.
import 'package:flutter/material.dart';
import 'package:mellotippet/force_upgrade/force_upgrade_page.dart';
class MellotippetApp extends StatelessWidget {
const MellotippetApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'My title',
home: const ForceUpgradePage(),
);
}
}
Step 7: Run and test the app
Run and test the app manually on both iOS and Android. Try different values for requiredMinimumVersion
and recommendedMinimumVersion
to verify that everything works as expected.
Required and recommended dialogs
Outro
I hope this tutorial is helpful. The source code can be found here: https://github.com/molundb/mellotippet.