firebase_update 2.0.0
firebase_update: ^2.0.0 copied to clipboard
Flutter force update, maintenance mode & patch notes via Firebase Remote Config. Real-time, server-controlled, with built-in UI and full customization.
firebase_update #
Flutter force update, maintenance mode, patch notes, and custom update UI driven by Firebase Remote Config.
Maintained by A Few Good Coders. Licensed under BSD-3-Clause.
firebase_update gives you one Remote Config payload and one package that can:
- present a non-dismissable force-update prompt for old builds
- show dismissible optional updates with snooze and skip-version behavior
- show maintenance mode when the config is received
- render patch notes in plain text or HTML
- react to real-time Remote Config changes without an app restart
- use cached app store hints without forcing extra config fetches
- use package-managed UI or fully custom surfaces
Version 2.0.0 #
firebase_update includes its update engine and UI directly, so it can be published and installed independently.
Startup, foreground, manual, and timer checks share a persistent cache and a 12-hour default minimum interval. Store-version hints no longer set Firebase's interval to zero. Realtime notifications activate the SDK's already downloaded template without another explicit fetch. SDK-initiated realtime downloads still count toward Firebase usage; this package cannot make them unlimited or cap other modules' requests.
A failed check keeps the last valid policy. Without valid policy data, state is idle and lastCheckError explains the failure. Null, empty, or malformed responses no longer silently clear an update gate. To clear all rules explicitly, send {"min_version":"", "latest_version":"", "maintenance_message":""}.
If your app already owns Remote Config fetching, use FirebaseUpdateConfig(appManaged: true) and forward the app's already activated JSON through applyPayload(). This mode performs no Firebase fetches, settings changes, or listener subscriptions. If you only want to preserve the host's SDK settings, pass its client as remoteConfig and set manageRemoteConfigSettings: false.
See the migration guide and architecture status for cache scope, source options, and release steps.
Screenshots #
Why this package #
Most update-gate packages handle only one narrow case. Production apps usually need all of these:
- real-time response during incidents
- optional updates that users can defer safely
- hard blocking for incompatible builds
- maintenance mode that wins over every other state
- an escape hatch for fully custom UI when branding or layout demands it
firebase_update is built around those transitions instead of treating them as separate packages or ad hoc dialogs.
Features #
- Real-time Remote Config listening with automatic retry and exponential backoff
- Optional update dialog or bottom sheet
- Force update dialog or bottom sheet — inescapable, re-shown on app resume
- Maintenance mode dialog, sheet, or fully custom blocking surface
- Store version hints — cached store checks can request a due config check without bypassing the fetch interval
- Periodic re-check timer — safety net for long-running sessions
- App lifecycle resume check — requests a check when the cache or retry deadline permits
- Snooze with version-aware reset
- Persistent skip-version
- Plain text and HTML patch notes
FirebaseUpdateBuilderfor reactive inline UIFirebaseUpdateCardfor drop-in in-app surfaces- Presentation theming, typography, labels, and icon overrides
- Analytics callbacks (
onDialogShown,onDialogDismissed,onSnoozed,onVersionSkipped) - Allowed flavors whitelist — silence update UI in staging builds
allowDebugBackfor debug-only escape on blocking overlaysonBeforePresenthook for preloading GIFs, images, or any async dependencies before showing an overlay- Shorebird patch surface support
Installation #
Requires Flutter 3.32+ and Dart 3.8+. Upgrading from 1.x? Read the 2.0 migration guide for changed caching, validation, and refresh behavior.
dart pub add firebase_update
This package expects Firebase to already be configured in your app.
Quick start #
Initialize after Firebase.initializeApp() and pass the same navigatorKey to MaterialApp.
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_update/firebase_update.dart';
import 'package:flutter/material.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await FirebaseUpdate.instance.initialize(
navigatorKey: rootNavigatorKey,
config: const FirebaseUpdateConfig(),
);
runApp(
MaterialApp(
navigatorKey: rootNavigatorKey,
home: const Placeholder(),
),
);
}
Remote Config schema #
Create a Remote Config parameter named firebase_update_config whose value is a JSON string.
{
"min_version": "2.5.0",
"latest_version": "2.6.0",
"optional_update_title": "Update available",
"optional_update_message": "A smoother release is ready.",
"force_update_message": "This release contains required security fixes.",
"maintenance_title": "Scheduled maintenance",
"maintenance_message": "",
"patch_notes": "Faster startup\nCleaner onboarding\nBug fixes",
"patch_notes_format": "text",
"store_url_android": "https://play.google.com/store/apps/details?id=com.example.app",
"store_url_ios": "https://apps.apple.com/app/id000000000"
}
Supported fields #
| Field | Purpose |
|---|---|
min_version |
Minimum supported version. If current < min_version, force update is shown. |
latest_version |
Latest available version. If current < latest_version and the minimum is satisfied, optional update is shown. |
maintenance_title |
Title for maintenance mode. |
maintenance_message |
Non-empty value activates maintenance mode. |
force_update_title |
Optional force-update title override. |
force_update_message |
Optional force-update body override. |
optional_update_title |
Optional optional-update title override. |
optional_update_message |
Optional optional-update body override. |
patch_notes |
Patch notes shown beside update prompts. |
patch_notes_format |
text or html. |
store_url_android, store_url_ios, store_url_macos, store_url_windows, store_url_linux, store_url_web |
Remote-configured store URLs that override local fallback URLs. |
State priority #
Only one state is active at a time:
- maintenance
- force update
- optional update
- up to date
Payload examples #
Optional update #
Use this when you want to encourage upgrades without blocking the app.
{
"min_version": "2.0.0",
"latest_version": "2.6.0",
"optional_update_title": "Update available",
"optional_update_message": "Version 2.6.0 is ready with a smoother experience.",
"patch_notes": "Faster startup · Cleaner onboarding · Bug fixes.",
"patch_notes_format": "text"
}
Force update #
Use this when the installed app version is no longer safe or compatible.
{
"min_version": "2.5.0",
"latest_version": "2.6.0",
"force_update_message": "This release contains required security fixes.",
"patch_notes": "<ul><li>Critical security patches</li><li>Required backend compatibility</li></ul>",
"patch_notes_format": "html"
}
Maintenance mode #
Use this when you need to temporarily gate the app without shipping a new build.
{
"maintenance_title": "Scheduled maintenance",
"maintenance_message": "We're upgrading our servers. We'll be back shortly."
}
Custom full-screen maintenance #
Use the same maintenance payload, but replace the package default surface with your own branded takeover.
FirebaseUpdateConfig(
onBeforePresent: (context, state) async {
await precacheImage(
const NetworkImage('https://example.com/maintenance.gif'),
context,
);
},
maintenanceWidget: (context, data) => MyMaintenanceTakeover(data: data),
)
Long patch notes #
When patch notes run long, the default UI collapses the content and expands it in-place with Read more / Show less.
{
"min_version": "2.0.0",
"latest_version": "2.6.0",
"optional_update_title": "Update available",
"optional_update_message": "Version 2.6.0 has arrived.",
"patch_notes": "Redesigned home screen with cleaner navigation bar\nDark mode across all screens and components\n40% faster app startup time\nNew notifications centre with grouped alerts and filters\nImproved search with smart suggestions and history\nOffline mode for core reading and browsing features\nFull VoiceOver and TalkBack accessibility support\nFixed checkout edge cases on older Android devices\nApple Pay and Google Pay now available in all regions",
"patch_notes_format": "text"
}
How it works #
State resolution #
Every time the payload changes, the package resolves exactly one state.
maintenance_message non-empty? -> maintenance
current < min_version? -> forceUpdate
current < latest_version? -> optionalUpdate
otherwise -> upToDate
Only one overlay is shown at a time. If the state changes while one is already visible, the existing overlay is dismissed first and the higher-priority one takes its place.
Snooze and skip-version #
Laterwith nosnoozeDurationdismisses the optional update for the current app sessionsnoozeDurationpersists the dismiss until the timer expires- snooze is version-aware, so a newer
latest_versionclears the older snooze immediately showSkipVersion: trueadds a persistent "Skip this version" action for optional updates
Common configurations #
Optional update with snooze #
FirebaseUpdateConfig(
snoozeDuration: const Duration(hours: 24),
showSkipVersion: true,
)
Force update as a bottom sheet #
FirebaseUpdateConfig(
useBottomSheetForForceUpdate: true,
)
Debug-only blocking escape #
FirebaseUpdateConfig(
allowDebugBack: true,
)
Preload assets before a custom overlay appears #
FirebaseUpdateConfig(
onBeforePresent: (context, state) async {
await precacheImage(
const NetworkImage('https://example.com/maintenance.gif'),
context,
);
},
maintenanceWidget: (context, data) => const MyFullScreenMaintenance(),
)
Reliability #
firebase_update combines config checks and presentation recovery to reduce missed update prompts. Delivery still depends on connectivity, provider quotas, fetch settings, and platform support; a client-side prompt cannot guarantee that every device has received the latest policy.
Store version fallback #
Store hints are checked on startup and, when enabled, on foreground transitions. Both store requests and Remote Config requests respect their cached intervals. A newer store version requests a due Remote Config check; it never forces a cache bypass or invents a mandatory update policy.
import 'dart:io';
import 'package:firebase_update/firebase_update.dart';
FirebaseUpdateConfig(
storeVersionSource: Platform.isAndroid
? PlayStoreVersionSource('com.example.app')
: AppStoreVersionSource(bundleId: 'com.example.app'),
checkStoreVersionOnResume: true,
)
Three built-in implementations are provided:
PlayStoreVersionSource('com.example.app')— reads a matching app's structured version metadata from the Play Store page. It returns unavailable when that metadata is missing. Store pages are not a stable version API; prefer a host-owned native Play update check orCallbackStoreVersionSourcebacked by your own API.AppStoreVersionSource(bundleId: 'com.example.app')— queries the iTunes Lookup API. Accepts an optionalcountryparameter (ISO 3166-1 alpha-2, defaults to'us') for apps published in specific storefronts.CallbackStoreVersionSource(() => myApi.getLatestVersion())— wraps anyFuture<String?>callback.
Periodic re-check #
An optional foreground timer requests config and store-hint checks when due. It pauses in the background and shares the normal minimum interval and retry cooldown. A six-hour timer with the default twelve-hour minimum interval does not cause a network fetch every six hours.
FirebaseUpdateConfig(
recheckInterval: const Duration(hours: 6),
)
Real-time listener resilience #
On a stream error, the package cancels that subscription and reconnects with exponential backoff and jitter, starting around 30 seconds and capping the base delay at 30 minutes. A stream that completes normally is not restarted; later due checks remain available. The Firebase adapter enables real-time listening on Android, iOS, and macOS. Other supported platforms use due checks.
Firebase reconnects with the device's cached template version. If a newer template exists, its real-time fetch bypasses the normal minimum interval; the package activates the result without adding another fetch. This also catches changes published while the app was closed, once connectivity and quota allow delivery. See Firebase's real-time documentation.
Force update presentation hardening #
Force update and maintenance dialogs have three additional safety mechanisms:
- Navigator mount retry — if the navigator isn't ready when the state is resolved (common during early bootstrap), the presenter retries across up to 50 frames (~800ms) instead of giving up after one
- Resume re-present — an
AppLifecycleListenerre-emits any active blocking state on every app foreground, so the dialog reappears if the user went to the store and came back without updating - 3-second safety net — if a blocking state is still active 3 seconds after emission, the presenter is invoked again to catch any silent failure
Reliability summary #
| Layer | Trigger | Catches |
|---|---|---|
| Real-time listener | Server change or reconnect | Newer policies while connected |
| Retry + backoff | Stream error | Dropped connections in release |
| Store version hints | Startup / enabled resume | Store changes can request a due config check |
| Lifecycle resume check | App foreground | Backgrounded for hours/days |
| Periodic re-check | Timer | Long-running foreground sessions |
| Navigator mount retry | Post-frame loop | Early bootstrap timing |
| Resume re-present | App foreground | User returned from store without updating |
| Blocking retry timer | 3-second delay | Any silent presenter failure |
Manual lifecycle pattern #
If you prefer to drive checks yourself instead of using the built-in timer:
late final AppLifecycleListener _lifecycleListener;
@override
void initState() {
super.initState();
_lifecycleListener = AppLifecycleListener(
onResume: () => FirebaseUpdate.instance.checkNow(),
);
}
@override
void dispose() {
_lifecycleListener.dispose();
super.dispose();
}
Custom UI #
You can replace any package-managed surface independently.
FirebaseUpdateConfig(
optionalUpdateWidget: (context, data) => MyOptionalUpdateSheet(data: data),
forceUpdateWidget: (context, data) => MyForceUpdateGate(data: data),
maintenanceWidget: (context, data) => MyMaintenanceTakeover(data: data),
)
Each builder receives FirebaseUpdatePresentationData, which already contains:
- resolved title and state
- primary and secondary labels
- wired callbacks like
onUpdateClick,onLaterClick, andonSkipClick - dismiss flags for package-managed containers
Reactive widgets #
Use FirebaseUpdateBuilder when you want to render update state inside your own screen.
FirebaseUpdateBuilder(
builder: (context, state) {
if (state.kind == FirebaseUpdateKind.optionalUpdate) {
return Text('Update ${state.latestVersion} is available');
}
return const SizedBox.shrink();
},
)
Or drop in FirebaseUpdateCard for a built-in inline surface.
API highlights #
FirebaseUpdate.instance #
initialize(...)checkNow()applyPayload(...)streamcurrentStatesnoozeOptionalUpdate(...)dismissOptionalUpdateForSession()skipVersion(...)clearSnooze()clearSkippedVersion()
FirebaseUpdateConfig #
Key options include:
listenToRealtimeUpdatesenableDefaultPresentationuseBottomSheetForOptionalUpdateuseBottomSheetForForceUpdateuseBottomSheetForMaintenancepresentationoptionalUpdateWidgetforceUpdateWidgetmaintenanceWidgetallowDebugBackonBeforePresentshowSkipVersionsnoozeDurationfallbackStoreUrlspreferencesStore
Example app #
The repo includes a complete example app under example/ that demonstrates:
- package-managed dialogs and sheets
- long patch-note layouts
- live JSON payload testing
- a custom full-screen maintenance takeover with preloaded network media
Testing and screenshots #
Run the package tests:
flutter test
Run the example integration tests:
cd example
flutter test integration_test/update_flow_test.dart -d <device-id>
Regenerate the README screenshots:
./scripts/take_screenshots.sh -d <device-id>