API-reference topic
API Reference
English | 简体中文
Every public API in flutter_patcher is exposed as a static member on the FlutterPatcher class.
The plugin only executes patch logic on Android. On iOS, Web, macOS, Windows, and Linux, calling these APIs is a no-op — they don't throw, they print a one-time warning on first call, and they return safe defaults.
Initialization
Call before runApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterPatcher.init();
runApp(const MyApp());
}
Most projects need no parameters. init() prepares the patch loader, the crash-protection state machine, and the boot diagnostic recorder. Repeated calls are safe.
If you want to enable signature verification, change the circuit-breaker threshold, or work around an unusual Flutter build, override the defaults:
await FlutterPatcher.init(
publicKeyBase64: 'MFkwEwYH...==',
maxCrashCount: 1,
strictSignature: true,
loaderFieldCandidates: ['flutterLoader'],
loaderFallbackHeuristic: false,
verifyAfter: const Duration(seconds: 5),
);
| Parameter | Description |
|---|---|
publicKeyBase64 |
Ed25519 public key. When PatchInfo.signature is empty, signature verification is skipped. A patch that ships with a signature but is loaded on a client without a configured public key is rejected. |
maxCrashCount |
Number of consecutive crashes that trips the patch. Default 1. |
strictSignature |
On API < 33 (no JDK Ed25519 support), reject signed patches instead of silently skipping verification. On API ≥ 33 the flag has no effect — native verification always runs. |
loaderFieldCandidates |
Candidate field names used to locate FlutterLoader. Rarely needs changing. |
loaderFallbackHeuristic |
If the candidates fail, use a heuristic last-resort scan. Off by default. |
verifyAfter |
Window after launch during which the patch is still considered "under verification". |
Check for updates (optional)
The plugin ships a minimal, optional check-update JSON protocol intended for quick onboarding, the example, and local debugging. In production you almost certainly already have your own update / staging / auth protocol — parse the response yourself, build a
PatchInfo, and skip this section.
If you do want to use the built-in protocol, call checkUpdate:
try {
final check = await FlutterPatcher.checkUpdate(
'https://api.example.com/patch/check',
headers: {'Authorization': 'Bearer $token'},
timeout: const Duration(seconds: 10),
);
if (check.hasUpdate) {
await FlutterPatcher.applyPatch(check.patch!);
}
} on PatcherException catch (e) {
log.warning('check update failed: ${e.message}');
}
checkUpdate returns a PatchCheckResult:
| Field | Type | Description |
|---|---|---|
hasUpdate |
bool |
Whether a patch is available. |
patch |
PatchInfo? |
The patch info; null when no update is available. |
If your server already speaks its own update protocol, skip checkUpdate and build a PatchInfo directly before calling applyPatch.
Apply a patch
There are two ways to apply a patch:
applyPatch: pass a URL and let the plugin download and verify it (recommended for most apps).applyPatchBytes: pass the patch bytes directly — useful for custom downloaders, asset-bundled patches, or isolate-based loading.
A successful apply takes effect on the next cold start. The current process is never modified in place.
Option 1: let the plugin download the patch
final result = await FlutterPatcher.applyPatch(
PatchInfo(
version: '1.0.0-h1',
patchUrl: 'https://cdn.example.com/libapp.so',
md5: '0123456789abcdef0123456789abcdef',
targetVersionCode: 100,
),
onProgress: (p) {
print('${p.phase.name}: ${p.fraction ?? "..."}');
},
);
if (result.ok) {
showRestartHint();
}
targetVersionCode is the host APK versionCode the patch was built for — not the patch version. If your live APK is versionCode = 100, every patch built for that APK should set targetVersionCode: 100.
If multiple APK versions are live at the same time, build and ship a separate patch per versionCode.
Option 2: apply patch bytes directly
final bytes = await loadPatchFromYourSource();
final result = await FlutterPatcher.applyPatchBytes(
bytes,
version: '1.0.0-h1',
targetVersionCode: 100,
onProgress: (p) => print(p.phase.name),
);
applyPatchBytes automatically computes the MD5, manages the temporary file, and reuses the same flow as applyPatch.
Handle the result
Both applyPatch and applyPatchBytes return a PatchApplyResult:
if (result.ok) {
// Patch persisted; takes effect on the next cold start.
showRestartHint();
} else {
switch (result.error!) {
case PatchApplyError.blacklisted:
// This patch previously caused a crash; stop delivering it.
break;
case PatchApplyError.network:
case PatchApplyError.ioError:
// Transient — retry later.
break;
case PatchApplyError.md5Mismatch:
// CDN content or server-side md5 may be inconsistent.
break;
case PatchApplyError.signatureInvalid:
// Treat as a security event.
break;
default:
log.warning('patch failed: ${result.error?.name} / ${result.message}');
}
}
result.message is for developers — don't surface it to end users.
Re-applying the same patch is safe; if it is already installed, the call returns ok = true.
Error codes
| Code | Meaning | Suggested handling |
|---|---|---|
invalidArgs |
Missing or malformed arguments | Inspect the server response |
blacklisted |
Patch hit the local blacklist | Stop delivering this patch |
network |
Download failed | Retry later |
md5Mismatch |
Downloaded MD5 does not match (only triggered when md5 is provided) | Check CDN / server-side md5 |
signatureInvalid |
Signature verification failed | Treat as a security event; do not retry |
ioError |
Disk write, rename, or permission failure | Retry later |
unknown |
Unclassified error | Inspect result.message |
Listening to progress
Besides onProgress, you can subscribe to the global broadcast stream:
FlutterPatcher.applyProgress.listen((p) {
print('${p.phase.name}: ${p.fraction}');
});
| Field | Description |
|---|---|
phase |
Current phase: downloading, verifying, finalizing. |
bytesReceived |
Bytes received so far; only meaningful while downloading. |
totalBytes |
Total bytes; -1 when the server omits Content-Length. |
fraction |
Download progress in 0.0 ~ 1.0; null when unknown. |
Roll back
await FlutterPatcher.rollback();
Rollback deletes the current patch. On the next cold start the app falls back to the version baked into the APK.
A manual rollback does not add the patch to the blacklist.
Manually report a successful boot
await FlutterPatcher.reportBootSuccess();
You usually don't need to call this. init() automatically reports a successful boot once the first frame has rendered.
Call it explicitly only when you want to confirm the patch is healthy with custom logic before the first frame:
await runLightweightSelfCheck();
await FlutterPatcher.reportBootSuccess();
Once the first frame has rendered, additional calls are no-ops.
Query state
final int? code = await FlutterPatcher.appVersionCode;
final String? version = await FlutterPatcher.currentVersion;
final String abi = await FlutterPatcher.deviceAbi;
| API | Description |
|---|---|
appVersionCode |
The current APK's versionCode. Uses longVersionCode on API 28+. |
currentVersion |
The patch version currently on disk (read from meta.json). Becomes readable immediately after a successful applyPatch, but the Flutter Engine only loads it on the next cold start. null when there is no patch. |
deviceAbi |
The current device ABI; useful for check-update requests. |
Boot diagnostics
After every cold start the native side records a single patch-load result. Read it via lastBootDiagnostic and report it:
final diag = await FlutterPatcher.lastBootDiagnostic;
if (diag != null && !diag.isHealthy) {
analytics.report('patch_dropped', {
'status': diag.status.name,
'patch_version': diag.patchVersion,
'crash_count': diag.crashCount,
'message': diag.message,
});
}
PatchBootDiagnostic fields:
| Field | Type | Description |
|---|---|---|
status |
PatchBootStatus |
The boot result. |
recordedAt |
DateTime |
When the diagnostic was recorded. |
patchVersion |
String? |
The patch version involved. |
patchTargetVersionCode |
int? |
The versionCode the patch was built for. |
appVersionCode |
int? |
The current APK's versionCode. |
crashCount |
int? |
Cumulative crash count. |
attemptedLoaderFields |
List<String>? |
Field names tried when the loader hook failed. |
message |
String? |
Developer-facing diagnostic text. |
isHealthy |
bool |
true when the status is patched or noPatch. |
PatchBootStatus values:
| Value | Meaning | Suggested handling |
|---|---|---|
patched |
Patch loaded successfully | Normal |
noPatch |
No patch; running APK built-in version | Normal |
droppedVersionCodeMismatch |
APK was upgraded; the old patch is no longer valid | Usually no alert needed |
droppedCircuitBreaker |
Patch caused repeated crashes and was tripped | Strong alert; stop delivering |
droppedSignatureInvalid |
Signature verification failed | Alert; investigate the source |
droppedMd5Mismatch |
Local file MD5 does not match the recorded MD5 | Report and investigate |
droppedMetaCorrupted |
Patch metadata is corrupt | Report and investigate |
hookInstallFailed |
FlutterLoader hook failed to install | Check Flutter version / loaderFieldCandidates |
unknown |
Unclassified error | Inspect message |
For interactive debugging, see example/lib/diag_card.dart — it renders the diagnostic on-device.
Blacklist
When a patch causes a boot crash or a verification failure, the plugin adds it to a local blacklist so the same bad patch is not retried.
final entries = await FlutterPatcher.blacklist;
for (final e in entries) {
print('${e.version} / ${e.md5} / ${e.reason} / ${e.blacklistedAt}');
}
To clear the blacklist (debug only):
await FlutterPatcher.clearBlacklist();
BlacklistEntry fields:
| Field | Type | Description |
|---|---|---|
version |
String |
Patch version. |
md5 |
String |
Patch file MD5. |
reason |
String |
Why the patch was blacklisted. |
blacklistedAt |
DateTime |
When the entry was recorded. |
Common reason values:
| Value | Description |
|---|---|
BOOT_CRASH |
Patch caused a boot crash. |
MD5_MISMATCH |
MD5 verification failed. |
SIGNATURE_INVALID |
Signature verification failed. |
PatchInfo
PatchInfo describes a patch ready to apply.
final patch = PatchInfo(
version: '1.0.0-h1',
patchUrl: 'https://cdn.example.com/libapp.so',
md5: '0123456789abcdef0123456789abcdef',
targetVersionCode: 100,
);
You can also build it from a server response:
final patch = PatchInfo.fromJson(json);
final map = patch.toJson();
fromJson accepts both camelCase and snake_case field names; unknown fields are kept in raw.
| Field | Type | Required | Description |
|---|---|---|---|
version |
String |
Yes | Patch identifier, an arbitrary string. |
patchUrl |
String |
Yes | Patch download URL. |
md5 |
String |
No | Patch MD5 (lower-case 32-hex). An empty string skips MD5 verification (and signature verification along with it). |
signature |
String |
No | Ed25519 signature, base64. Empty disables signature verification. Only effective when md5 is non-empty. |
targetVersionCode |
int? |
Recommended | Host APK versionCode the patch is built for. |
raw |
Map<String, dynamic> |
No | Original fields preserved by fromJson. |
Exception behavior
Only checkUpdate throws PatcherException, typically for network failures or unparsable JSON.
Every other API reports outcomes through return values rather than exceptions.
try {
final check = await FlutterPatcher.checkUpdate(url);
} on PatcherException catch (e) {
log.warning(e.message);
}
pack CLI
flutter_patcher:pack extracts libapp.so from a release APK and emits the patch metadata.
dart run flutter_patcher:pack \
--apk build/app/outputs/flutter-apk/app-release.apk \
--version 1.0.0-h1 \
--target-version-code 100
| Flag | Description |
|---|---|
--apk <path> |
Required. Path to the release APK. |
--version <string> |
Required. Patch identifier. |
--target-version-code <int> |
Required. Host APK versionCode the patch targets. |
--abi <string> |
Optional. Defaults to the first match among arm64-v8a, armeabi-v7a, x86_64. |
--out <dir> |
Optional. Output directory; defaults to dist/. |
--target-version-code binds the patch to a specific base APK already installed on the user's device. For example:
- The live APK is
versionCode = 100 - You are publishing patch
1.0.0-h1for that APK --target-version-codeshould be100
If the APK is upgraded to a new versionCode, old patches expire automatically.
If multiple versionCodes are live at the same time, build and ship a separate patch per base.
Output:
dist/
├── libapp.so
└── manifest.json
Upload libapp.so and manifest.json to your CDN, then return them from your update endpoint.
Performance and supported range
Performance impact
| Metric | Impact |
|---|---|
| APK size delta | ~80–120 KB |
| Cold-start delta | ~5–15 ms |
| Runtime memory | No additional resident footprint after patch load |
| Patch file size | Typically 5–15 MB |
Numbers measured on Pixel 6 / Flutter 3.24. Real-world results vary with device, Flutter version, and build configuration.
Supported range
| Dimension | Requirement |
|---|---|
| Platform | Android |
Android minSdk |
24 |
| Flutter | >=3.3.0; loader hook verified on 3.19 ~ 3.38 |
| ABI | armeabi-v7a / arm64-v8a / x86_64 |
| NDK | 27.0.12077973+ |
| AGP | 8.11.1+ |
| Kotlin | 2.2.20+ |
| Java / JVM | 17 |
On non-Android platforms every API is a no-op: a one-time warning is logged and safe defaults are returned. No exceptions are thrown.
Version compatibility
- During the
0.xseries the API may still change; pin a version inpubspec.yaml. PatchBootStatusand blacklistreasonvalues are forward-compatible: new values are mapped tounknownby older SDKs.PatchInfo.fromJsonaccepts both camelCase and snake_case names; unknown fields are preserved inrawand don't break parsing.
Classes
- FlutterPatcher Architecture API-reference Crash-protection
- Android libapp.so 热更新入口。