A package for showing a force update prompt that is controlled remotely.
Future<String>
.Depend on it:
dependencies:
force_update_helper:
Use it by adding a ForceUpdateWidget
to your MaterialApp
's builder property:
void main() {
runApp(const MainApp());
}
final _rootNavigatorKey = GlobalKey<NavigatorState>();
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _rootNavigatorKey,
builder: (context, child) {
return ForceUpdateWidget(
navigatorKey: _rootNavigatorKey,
forceUpdateClient: ForceUpdateClient(
// * Real apps should fetch this from an API endpoint or via
// * Firebase Remote Config
fetchRequiredVersion: () => Future.value('2.0.0'),
// * Example ID from this app: https://fluttertips.dev/
// * To avoid mistakes, store the ID as an environment variable and
// * read it with String.fromEnvironment
iosAppStoreId: '6482293361',
),
allowCancel: false,
showForceUpdateAlert: (context, allowCancel) => showAlertDialog(
context: context,
title: 'App Update Required',
content: 'Please update to continue using the app.',
cancelActionText: allowCancel ? 'Later' : null,
defaultActionText: 'Update Now',
),
showStoreListing: (storeUrl) async {
if (await canLaunchUrl(storeUrl)) {
await launchUrl(
storeUrl,
// * Open app store app directly (or fallback to browser)
mode: LaunchMode.externalApplication,
);
} else {
log('Cannot launch URL: $storeUrl');
}
},
onException: (e, st) {
log(e.toString());
},
child: child!,
);
},
home: const Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
Note that in order to show the update dialog, a root navigator key needs to be added to MaterialApp
(this is the same technique used by the upgrader package).
Unlike the upgrader package, this package does not use the app store APIs to check if a newer version is available.
Instead, it allows you to store the required version remotely (using a custom backend or Firebase Remote Config), and compare it with the current version from your pubspec.yaml
.
Here's how you may use this in production:
required_version
endpoint in your custom backend or via Firebase Remote ConfigThe package is made of two classes: ForceUpdateClient
and ForceUpdateWidget
.
ForceUpdateClient
class fetches the required version and compares it with the current version from package_info_plus. Versions are compared using the pub_semver package.fetchRequiredVersion
callback should fetch the required version from an API endpoint or Firebase Remote Config.iosAppStoreId
, otherwise the force upgrade alert will not show. I recommend storing an APP_STORE_ID
as an environment variable that is set with --dart-define
or --dart-define-from-file
and read with String.fromEnvironment
.allowCancel: true
to the ForceUpdateWidget
and use it to add a cancel button to the alert dialog. This will make the alert dismissable, but the prompt will still show on the next app start.onException
handler. Alternatively, omit the onException
and handle exceptions globally.AndroidManifest.xml
: <queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"/>
</intent>
</queries>
The force update logic is triggered in two cases:
Then, the update alert will show if all these conditions are true:
requiredVersion
is fetched successfullyrequiredVersion
is greater than the currentVersion
iosAppStoreId
is a non-empty stringIf the user clicks on "Update Now" and lands on the app store page but does not update the app, the force update alert will show again when returning to the app.
If the update alert shows on Android and the back button is pressed, it will be shown again unless allowCancel
is true
.
Once you have created your app in App Store Connect, you can grab the app ID from the browser URL:
Make sure to set the correct iosAppStoreId
before releasing the first version of your app, otherwise users on old version won't be able to update.
The example app shows how to fetch some remote config JSON from a RemoteConfigGistClient
:
ForceUpdateWidget(
navigatorKey: _rootNavigatorKey,
forceUpdateClient: ForceUpdateClient(
fetchRequiredVersion: () async {
// * Fetch remote config from an API endpoint.
// * Alternatively, you can use Firebase Remote Config
final client = RemoteConfigGistClient(dio: Dio());
final remoteConfig = await client.fetchRemoteConfig();
return remoteConfig.requiredVersion;
},
// * Example ID from this app: https://fluttertips.dev/
// * To avoid mistakes, store the ID as an environment variable and
// * read it with String.fromEnvironment
iosAppStoreId: '6482293361',
),
allowCancel: false,
showForceUpdateAlert: (context, allowCancel) => showAlertDialog(
context: context,
title: 'App Update Required',
content: 'Please update to continue using the app.',
cancelActionText: allowCancel ? 'Later' : null,
defaultActionText: 'Update Now',
),
showStoreListing: (storeUrl) async {
if (await canLaunchUrl(storeUrl)) {
await launchUrl(
storeUrl,
// * Open app store app directly (or fallback to browser)
mode: LaunchMode.externalApplication,
);
} else {
log('Cannot launch URL: $storeUrl');
}
},
onException: (e, st) {
log(e.toString());
},
child: child!,
)
The RemoteConfigGistData
class can be used to fetch and parse some JSON in this format:
{
"config" : {
"required_version": "2.0.0"
}
}
Here's the reference RemoteConfigGistData
class:
import 'dart:convert';
import 'package:dio/dio.dart';
class RemoteConfigGistData {
RemoteConfigGistData({required this.requiredVersion});
final String requiredVersion;
factory RemoteConfigGistData.fromJson(Map<String, dynamic> json) {
final requiredVersion = json['config']?['required_version'];
if (requiredVersion == null) {
throw FormatException('required_version not found in JSON: $json');
}
return RemoteConfigGistData(requiredVersion: requiredVersion);
}
}
/// An API client class for fetching a remote config JSON from a GitHub gist
class RemoteConfigGistClient {
const RemoteConfigGistClient({required this.dio});
final Dio dio;
/// Fetch the remote config JSON
Future<RemoteConfigGistData> fetchRemoteConfig() async {
// TODO: Update this with your GitHub username
const owner = 'bizz84';
// TODO: Update this with your gist IDs
const gistId = 'e5b8041b35c58a3eba2baa23096d1678';
// TODO: Update this with your gist file name
const fileName = 'app_name_remote_config.json';
const url =
'https://gist.githubusercontent.com/$owner/$gistId/raw/$fileName';
final response = await dio.get(url);
final jsonData = jsonDecode(response.data);
return RemoteConfigGistData.fromJson(jsonData);
}
}
For more info, see this example app:
The package comes with a sample server-side app that implements a required_version
endpoint using Dart Shelf.
This can be used as part of the force update logic in your Flutter apps.
For more info, see this example app:
I created this package so I can reuse the force update logic in my own apps.
While you're welcome to suggest improvements, I don't want the package to become bloated, and I only plan to make changes that suit my needs.
If the package doesn't suit your use case, consider forking and maintaining it yourself.