Over-the-air code push updates for Flutter apps. Ship Dart fixes to your existing features between store releases, with signed patches and automatic crash recovery.
This package provides the runtime SDK that you add to your Flutter app.
It communicates with a code-push-enabled Flutter engine (installed via the
fcp CLI) to check for updates, download patches, apply them, and
automatically roll back if something goes wrong.
Code push updates your app's Dart code at runtime through a virtual-machine-based update mechanism. Your app's native code, resources, and permissions are never modified -- the app itself is only ever updated through the store.
Store policies govern this kind of mechanism, and you are the developer of record for your app. Review Google Play's Device and Network Abuse policy and App Store Review Guideline 3.3.2, and make your own distribution decision. Distribution channels outside the app stores (enterprise/MDM, alternative stores, direct APK) carry no such restriction.
If you want over-the-air updates disabled specifically for Play Store
installs, set disableOnPlayStoreInstalls: true when initializing the SDK
-- the app then updates only through the store on Play-installed devices,
while other channels keep code push.
- Automatic update checking -- checks on launch, periodically, and on app resume
- Live patching (iOS) -- bytecode data modules load without restarting the app
- Restart-based patching (Android/Desktop) -- ELF patches applied on next cold restart
- Crash protection with auto-rollback -- reverts bad patches after repeated failed boots
- RSA signature verification -- optional cryptographic signing for patch integrity
- SHA-256 hash verification -- always-on integrity check for every patch
- Debug status bar -- opt-in overlay showing real-time code push status
fcpCLI installed (dart pub global activate flutter_compile)- A FlutterPlaza Code Push account -- run
fcp codepush login - Code-push-enabled engine artifacts -- run
fcp codepush setup
dependencies:
flutterplaza_code_push: ^0.1.0flutter pub getWrap your root widget with CodePushOverlay. This handles the entire update
lifecycle automatically -- checking for updates, downloading patches, and
showing a restart banner when an update is ready.
import 'package:flutterplaza_code_push/flutterplaza_code_push.dart';
void main() {
runApp(
CodePushOverlay(
config: CodePushConfig(
serverUrl: 'https://your-server.com',
appId: 'your-app-id',
releaseVersion: '1.0.0+1',
),
child: MyApp(),
),
);
}That is the only change needed. The overlay checks for updates on launch, every
4 hours (configurable), and whenever the app returns from the background. On
Android/desktop, when a patch is downloaded and installed a banner appears
prompting the user to restart. On iOS the first patch applies live without a
restart (no banner) — see the CodePushOverlay reference below.
During development, you can enable a small status bar at the top of the screen that shows what code push is doing in real time:
CodePushOverlay(
config: CodePushConfig(
serverUrl: 'https://your-server.com',
appId: 'your-app-id',
releaseVersion: '1.0.0+1',
),
showDebugBar: true, // Shows "CP: Checking server...", "CP: Restart to apply", etc.
child: MyApp(),
)Starts the automatic update lifecycle. Call once at app startup. This is what
CodePushOverlay calls internally -- you only need this if you are not
using the overlay widget.
CodePush.init(
serverUrl: 'https://your-server.com',
appId: 'your-app-id',
releaseVersion: '1.0.0+1',
interval: Duration(hours: 4), // optional, default 4 hours
channel: 'production', // optional, default 'production'
onUpdateReady: () {
// Called when a patch is installed and a restart is needed.
},
);What init does:
- Runs crash protection checks (auto-rollback if needed)
- Checks for updates immediately
- Checks periodically at the configured interval
- Reports launch success after a 10-second grace period
Stops automatic update checking and cancels the launch timer.
CodePush.dispose();Checks the server for updates, downloads, and installs if available. Returns
true if a patch was installed.
final installed = await CodePush.checkAndInstall(
serverUrl: 'https://your-server.com',
appId: 'your-app-id',
releaseVersion: '1.0.0+1',
channel: 'production',
onUpdateReady: () {
// Prompt user to restart (Android/Desktop only).
},
);On iOS, bytecode patches are loaded live without a restart. On Android and
desktop, onUpdateReady is called so you can prompt the user to restart.
Checks the engine for available updates without downloading.
final UpdateInfo info = await CodePush.checkForUpdate();
if (info.isUpdateAvailable) {
print('Patch ${info.patchVersion} available (${info.downloadSize} bytes)');
}Installs a patch from raw bytes. Use this when you download the patch yourself (for example, via your own HTTP client).
final Uint8List patchBytes = await myHttpClient.downloadPatch(url);
await CodePush.installPatch(patchBytes);The engine verifies patch integrity (SHA-256 hash, optional RSA signature) before installing. The patch takes effect on the next cold restart.
Rolls back to the base release by removing the active patch. Takes effect on next cold restart.
await CodePush.rollback();Triggers a cold restart of the app. On next launch, the engine loads the installed patch.
CodePush.restart();Returns whether the app is currently running with a code push patch. Note this
reads the engine channel, which is disabled on iOS — it always returns
false on iOS even while a patch is active. For an iOS patch signal, use
CodePush.moduleResult (see below).
final bool patched = await CodePush.isPatched;Returns information about the currently installed patch, or null if none
is active.
final PatchInfo? patch = await CodePush.currentPatch;
if (patch != null) {
print('Version: ${patch.version}');
print('Installed at: ${patch.installedAt}');
}Returns the release version string for this app build.
final String version = await CodePush.releaseVersion;A ValueNotifier<String> that broadcasts what code push is currently doing —
for debug UIs and logging. Each step overwrites it, so it is a transition
signal, not a level you can poll.
CodePush.status.addListener(() {
print('Code push status: ${CodePush.status.value}');
});Values include: init, Checking server..., Downloading patch...,
Patch active, No update (204), Restart to apply, etc.
Don't use this as an app-facing "a patch loaded" signal. CodePushOverlay
latches the Patch active edge internally, but on that transition it re-keys
its child subtree, disposing any latch a widget under it holds — and the edge
never returns. For an app-facing iOS signal use CodePush.moduleResult (a
level, which survives the re-key), below.
A ValueNotifier<Object?> that holds the result from the last loaded bytecode
module — the app-facing iOS patch signal. It is a level (unlike the
status edge and isPatched, which reads false on iOS), so it survives the
overlay re-keying its subtree when a patch loads. Listen with a
ValueListenableBuilder<Object?> to drive OTA UI without a restart.
Caveat: it reads null in two cases — a patch that loaded with no return value
(a pure code patch), and after a revert to baseline (a deliberate rollback or
an automatic post-failure revert, while the module stays resident). So it
signals content, not merely that a patch is active.
ValueListenableBuilder<Object?>(
valueListenable: CodePush.moduleResult,
builder: (context, result, _) {
if (result is Map<String, dynamic>) {
// Use the patch data to update your UI.
}
return const SizedBox.shrink();
},
);The recommended way to integrate code push. Wraps your app widget, manages the full update lifecycle, and shows a restart banner when an update is ready.
CodePushOverlay(
config: CodePushConfig(...),
child: MyApp(),
showDebugBar: false, // optional, shows status bar at top
bannerBuilder: (context, onRestart, onDismiss) {
// optional, return a custom banner widget — must return a widget;
// return const SizedBox.shrink() to show nothing. The builder call
// itself is the "patch ready" signal, but it runs during build and
// may run many times — no side effects here. To drive your own UI,
// return your own widget and wire the handed onRestart/onDismiss to
// your UI's actions from its initState or a post-frame callback
// (onRestart hard-restarts the process, so gate it behind a user tap).
return MyCustomBanner(onRestart: onRestart, onDismiss: onDismiss);
},
)The builder is the "patch ready" signal on the Android/desktop path. On
iOS a freshly downloaded patch is applied to the running VM without a
restart, so the builder is not invoked for it — on iOS the banner appears only
when a different patch arrives while one is already loaded, or when a resident
patch is re-offered after a rollback reverted the app-facing content. For an
app-facing iOS patch signal, listen to CodePush.moduleResult (a level — it
survives the overlay re-keying its subtree when a patch loads; null for a
patch with no return value and after a revert to baseline, so it signals
content, not merely "a patch is active"). Don't latch on CodePush.status from a widget under the overlay
(Patch active is a fleeting edge and the re-key disposes the latch), and
don't use CodePush.isPatched — on iOS it always reads false.
The builder runs during build and may run many times, so keep side effects
out of it: calling onDismiss from inside the builder does nothing useful (the
banner lingers until the next rebuild), and showDialog/navigation throw
(calling onDismiss from a user action in your returned widget does hide the
banner). When the overlay wraps your MaterialApp (as above), the builder's
context has no Navigator/Overlay ancestor — drive dialogs and routes from a
navigatorKey on your MaterialApp (or a context inside the app), not the
builder's context.
Note: CodePushOverlay calls CodePush.init itself in its initState.
Calling CodePush.init(...) in main() as well — even without an
onUpdateReady: — starts a second check cycle that races the overlay's and
usually installs the first patch with no banner that session. Pass config:
to the overlay and don't call CodePush.init in main(). To own the update
lifecycle instead, call CodePush.init / CodePush.checkAndInstall directly
instead of using the overlay.
Configuration object for CodePushOverlay.
| Parameter | Type | Default | Description |
|---|---|---|---|
serverUrl |
String |
required | Your code push server URL |
appId |
String |
required | Your app's identifier |
releaseVersion |
String |
required | The current release version (e.g. 1.0.0+1) |
checkInterval |
Duration |
4 hours | How often to check for updates |
channel |
String |
production |
The update channel |
disableOnPlayStoreInstalls |
bool |
false |
When true, over-the-air updates are disabled on devices where the app was installed from the Play Store; other install channels keep code push. |
A widget that rebuilds when a bytecode module result becomes available. Use this to apply OTA patches to specific parts of your UI.
CodePushPatchBuilder(
patchKey: 'promo_banner',
builder: (context, patchData, child) {
if (patchData == null) return child!;
return Text(patchData);
},
child: Text('Default content'),
)Only string module results reach the builder — a Map/List payload (the
common iOS shape) yields the baseline branch (patchData == null). If
patchKey is provided, the builder only receives data from string results
that start with that key (e.g. promo_banner:Hello World passes
Hello World to the builder). If patchKey is null, every non-empty string
result is passed through as-is (an empty string yields the baseline branch).
| Field | Type | Description |
|---|---|---|
isUpdateAvailable |
bool |
Whether an update is available |
patchVersion |
String? |
Version string of the available patch |
downloadSize |
int? |
Size of the patch in bytes |
| Field | Type | Description |
|---|---|---|
version |
String |
Patch version string |
installedAt |
DateTime |
When the patch was installed |
Thrown when a code push operation fails. Contains a message field describing
the error.
Code push includes automatic crash protection to prevent a bad patch from bricking your app. Here is how it works:
- Boot counter -- Each time the app starts with an active patch, a boot counter is incremented.
- Grace period -- After 10 seconds of successful execution, the launch is marked as successful and the boot counter resets to zero.
- Auto-rollback -- If the app fails to survive the grace period 3 times in a row, the patch is automatically removed on the next launch, reverting the app to its base release.
This works on all platforms:
- Android and Desktop -- The engine handles crash protection natively in C++.
- iOS -- Crash protection runs in Dart (the engine's native updater is disabled on iOS due to Apple Clang LTO constraints).
No configuration is needed. Crash protection is always active when a patch is installed.
Every patch is verified before installation:
The engine computes a SHA-256 hash of every downloaded patch and verifies it against the expected hash. Tampered or corrupted patches are rejected.
For additional security, you can configure RSA signature verification. When enabled, the engine verifies that each patch was signed with your private key before installing it.
iOS -- Add your RSA public key to Info.plist:
<key>FLTCodePushPublicKey</key>
<string>-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhki...your key here...
-----END PUBLIC KEY-----</string>Android -- Add your RSA public key to codepush.yaml in your project root:
public_key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhki...your key here...
-----END PUBLIC KEY-----When a public key is configured, patches without a valid signature are rejected. When no public key is configured, signature verification is skipped (SHA-256 hash verification still applies).
| Platform | Patch Type | Restart Required | Live Reload |
|---|---|---|---|
| iOS | Bytecode | No (data modules) | Yes |
| Android | ELF | Yes | No |
| Desktop | ELF | Yes | No |
- iOS: Bytecode patches are loaded as data modules at runtime. The app does
not need to restart. Listen to
CodePush.moduleResultfor any patch payload;CodePushPatchBuilderis a convenience wrapper for string payloads only. - Android and Desktop: ELF patches are written to disk and loaded by the
engine on the next cold restart. The
onUpdateReadycallback (or the overlay banner) lets you prompt the user to restart.
1. fcp codepush login # one-time auth
2. fcp codepush setup # download engine artifacts
3. fcp codepush init # register app on server
4. fcp codepush release --build --platform apk # upload baseline
5. # ... make code changes ...
6. fcp codepush patch --build --platform apk --release-id <id> # upload patch
7. App detects update, downloads, installs, and restarts (or live-loads on iOS)
BSD 3-Clause. See LICENSE for details.