fl_updater
A lightweight, cost-conscious Flutter plugin for Firebase Remote Config-driven app updates. Supports automatic launch checks, soft updates with per-version snoozing, non-dismissible force updates, and native App Store / Google Play Store redirection.
✨ Features
- 🚀 Declarative Wrapper: Wrap your
MaterialAppwithFlUpdaterWrapperfor zero-boilerplate launch checks. - ⚡ Imperative API: Use
FlUpdater().checkForUpdate()orFlUpdater().showUpdateDialog()for manual checks (e.g. from a settings screen). - 🔄 Soft & Force Updates:
- Soft updates: Optional update prompt with a "Later" button.
- Force updates: Mandatory blocking dialog (
canPop: false) when the installed version is belowfl_updater_min_version.
- ⏰ Smart Snoozing: Dismissing a soft update snoozes it for a configurable duration (default: 3 days). Snooze is scoped per version, so releasing a newer update immediately prompts the user again.
- 💰 Cost-Conscious Architecture: Designed for Firebase Remote Config usage-based pricing:
- Debug mode disabled by default: Prevents development hot restarts from consuming Remote Config quotas.
- Cached fetches: Configurable
minimumFetchInterval(default: 1 hour) ensures throttled network requests.
- 🏬 Native Store Redirection: Opens the platform's native store page (Apple App Store on iOS, Google Play Store on Android).
- 🎨 Fully Customizable UI: Style the built-in Material dialog with
FlUpdaterDialogStyle, or supply your own custom UI viadialogBuilder.
📦 Installation
Add fl_updater and firebase_core to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
fl_updater: ^0.0.1
firebase_core: ^4.13.0 # or latest
Then ensure Firebase is initialized in your main() method:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
🔧 Firebase Remote Config Setup
Configure update parameters in your Firebase Console → Remote Config.
1. Parameters
Create the following two parameters:
| Parameter Key | Type | Description | Default Value |
|---|---|---|---|
fl_updater_latest_version |
String | The latest published version available in stores. | "0.0.0" |
fl_updater_min_version |
String | The minimum supported version below which an update is forced. | "0.0.0" (or Use in-app default) |
2. Platform Conditional Values (fl_updater_android & fl_updater_ios)
In Firebase Console, you can define targeting conditions:
fl_updater_android: Condition rule:device.os == 'android'fl_updater_ios: Condition rule:device.os == 'ios'
Then add conditional values to fl_updater_latest_version and fl_updater_min_version:
- For Android (
fl_updater_android): e.g., latest version"2.5.0", min version"2.0.0" - For iOS (
fl_updater_ios): e.g., latest version"2.4.0", min version"2.1.0"
Firebase Remote Config automatically evaluates these conditions per device on fetch and serves the appropriate values to fl_updater. All other keys are ignored.
3. Store Redirection Identifiers
Store identifiers are configured directly in Dart code (via the wrapper or API call):
- iOS (
iosAppId): Numeric Apple App Store ID (e.g.,'123456789'). - Android (
androidPackageId): Package name (e.g.,'com.example.app'). Defaults to the host app package name if omitted.
🚀 Usage
1. Automatic Usage (Recommended)
Wrap your MaterialApp with FlUpdaterWrapper inside the builder callback:
import 'package:flutter/material.dart';
import 'package:fl_updater/fl_updater.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => FlUpdaterWrapper(
iosAppId: '123456789',
androidPackageId: 'com.example.app', // Optional: defaults to host package
child: child!,
),
home: const HomePage(),
);
}
}
This checks Remote Config once when the app is launched and displays the update dialog if an update is available and not currently snoozed.
2. Imperative / Manual Usage
Trigger an update check manually, such as from an "About" or "Settings" screen:
final updater = FlUpdater();
// Check and show dialog if an update is available:
await updater.showUpdateDialog(
context,
iosAppId: '123456789',
androidPackageId: 'com.example.app',
);
Or check status without displaying a UI:
final updater = FlUpdater();
final info = await updater.checkForUpdate(
iosAppId: '123456789',
);
print('Current: ${info.currentVersion}');
print('Latest: ${info.latestVersion}');
print('Status: ${info.status}'); // UpdateStatus.none, soft, or force
🎨 Customization
Styling the Default Dialog
Customize colors, typography, buttons, shapes, and icons using FlUpdaterDialogStyle:
FlUpdaterWrapper(
iosAppId: '123456789',
title: 'Exciting New Update!',
message: 'We added new features and performance improvements.',
updateButtonText: 'Update Now',
laterButtonText: 'Not Now',
style: FlUpdaterDialogStyle(
backgroundColor: Colors.white,
titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
messageStyle: const TextStyle(color: Colors.black87),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
icon: const Icon(Icons.system_update, size: 40, color: Colors.blue),
),
child: child!,
)
Providing a Custom Update UI (dialogBuilder)
Replace the built-in dialog entirely with your own custom widget or bottom sheet:
FlUpdaterWrapper(
iosAppId: '123456789',
dialogBuilder: (context, info, onUpdate, onLater) {
final isForce = info.status == UpdateStatus.force;
return AlertDialog(
title: Text('Version ${info.latestVersion} Available'),
content: Text('You are on ${info.currentVersion}. Please update to continue.'),
actions: [
if (!isForce)
TextButton(
onPressed: onLater, // Snoozes the update and dismisses dialog
child: const Text('Remind me later'),
),
ElevatedButton(
onPressed: onUpdate, // Redirects to App/Play Store
child: const Text('Update'),
),
],
);
},
child: child!,
)
⏰ Snoozing Behavior
When a soft (optional) update is available, tapping the "Later" button snoozes update prompts for snoozeDuration (default: 3 days).
- The snooze is persisted locally via
SharedPreferences. - Snoozes are scoped to the latest version. When you publish a newer version in Remote Config, the active snooze is automatically invalidated.
- Force updates always bypass snooze and cannot be dismissed.
FlUpdaterWrapper(
snoozeDuration: const Duration(days: 7), // Snooze for 1 week
child: child!,
)
Resetting Snooze (For Debugging & Testing)
You can automatically clear the snooze store on every app launch during development:
FlUpdaterWrapper(
enableInDebugMode: true,
clearSnoozeInDebugMode: true, // Clears previous snoozes on app launch in debug mode
child: child!,
)
Or reset it manually via code:
// Globally clear active snooze state:
await FlUpdater.clearSnoozeStore();
// Or on an instance:
final updater = FlUpdater();
await updater.clearSnooze();
💰 Fetch Behavior & Quota Optimization
To safeguard your Firebase Remote Config quota and avoid unintended billing:
- Disabled in Debug Mode by Default:
kDebugModedisables Remote Config fetching entirely so that frequent hot restarts do not burn quotas. To test in debug mode or staging, passenableInDebugMode: true:FlUpdaterWrapper( enableInDebugMode: true, // Opt-in for debug/staging builds child: child!, ) - Fetch Interval Throttling: The
minimumFetchInterval(default: 1 hour) prevents frequent network queries. Repeated checks within this duration use the Firebase cached values.FlUpdaterWrapper( minimumFetchInterval: const Duration(minutes: 30), child: child!, )
⚡ Real-Time Remote Config Updates
fl_updater listens to Firebase Remote Config updates in real time via onConfigUpdated:
- When you publish changes to
fl_updater_latest_versionorfl_updater_min_versionin the Firebase Console, the new config is activated immediately. - The update status is evaluated without waiting for
minimumFetchIntervalto expire. - Active snoozes are automatically cleared so users are prompted for the newly published version right away.
- If the new version requires an update, the update dialog appears instantly for active users.
Real-time updates are enabled by default (listenForRealtimeUpdates: true). You can disable them if needed:
FlUpdaterWrapper(
listenForRealtimeUpdates: false, // Only check on app launch
child: child!,
)
🪵 Diagnostic Logging
Logging is disabled by default to keep console and production outputs clean. You can enable diagnostic logging in several ways:
1. Globally
void main() {
FlUpdater.enableLogging = true;
runApp(const MyApp());
}
2. Per Wrapper or Method Call
FlUpdaterWrapper(
enableLogging: true,
child: child!,
)
📖 API Reference
FlUpdaterWrapper & FlUpdater.showUpdateDialog
| Property | Type | Default | Description |
|---|---|---|---|
iosAppId |
String? |
null |
Numeric Apple App Store ID (required for iOS). |
androidPackageId |
String? |
null |
Google Play Store package name (defaults to host app). |
navigatorKey |
GlobalKey<NavigatorState>? |
null |
Optional explicit key for the root Navigator. |
snoozeDuration |
Duration |
Duration(days: 3) |
How long to snooze soft updates when dismissed. |
minimumFetchInterval |
Duration |
Duration(hours: 1) |
Throttling interval for Firebase Remote Config fetches. |
enableInDebugMode |
bool |
false |
Enable checks in kDebugMode. |
clearSnoozeInDebugMode |
bool |
false |
Automatically clear saved snooze state on launch in debug mode. |
listenForRealtimeUpdates |
bool |
true |
Instantly activate and check updates on Remote Config publish. |
enableLogging |
bool? |
null |
Enable diagnostic console logs for troubleshooting. |
title |
String? |
'Update available' |
Dialog title text. |
message |
String? |
null |
Dialog message body text. |
updateButtonText |
String? |
'Update' |
Label for the update button. |
laterButtonText |
String? |
'Later' |
Label for the later/snooze button. |
style |
FlUpdaterDialogStyle? |
null |
Style configuration for the default dialog. |
dialogBuilder |
FlUpdaterDialogBuilder? |
null |
Custom builder to provide your own dialog UI. |
📱 Example App
Check out the example directory for a complete sample app demonstrating both automatic wrapper and manual checking with Firebase Remote Config.
To run the example app:
cd example
flutter run
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Libraries
- fl_updater
- A Flutter plugin that provides Firebase Remote Config-driven app update checking, snoozable soft updates, blocking force updates, and native App Store / Google Play Store redirection.