跪拜 Guibai
← All articles
Flutter · HotFix

flutter_patcher 1.0.0 brings store-compliant hot updates to Flutter via Dart dynamic modules

By 雪铃儿 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Flutter teams can now hot-fix production bugs on both iOS and Android without waiting through a full release cycle, and without routing patch traffic through a third-party cloud or paying per-install fees. The trade-off is a narrow hot-update scope: anything touching native code, plugins, or FFI still requires a regular release.

Summary

Flutter's AOT compilation means a one-line bug fix still forces a full rebuild, app-store review, and user upgrade cycle that can stretch to a week. Existing workarounds are weak: custom Dart interpreters or DSL delivery restrict how you write code, and swapping .so files only works on Android while violating Google Play. Shorebird is technically strong but locked to its cloud, bills per patch install, and can fail corporate compliance checks.

flutter_patcher builds on Dart's experimental dynamic modules, embedding an interpreter in the app to load Dart bytecode at runtime. To get around the official restriction that loaded code can't override existing declarations, it renames the entire app package to patch.* and switches the startup entry to the patch's main function. The Flutter framework, SDK, and native plugins stay as machine code in the host package; only business logic and pure Dart dependencies go into the patch, so a one-line text change compiles to about 19 KB.

It's designed around store policy: bytecode runs in the app's built-in interpreter, no native executables are delivered, and any HTTPS static host (S3, R2, OSS, GCS) works for self-hosting. Patches are signed, a startup crash triggers automatic fallback to the original package, and the tool statically blocks unsupported changes like FFI calls or Flutter version upgrades. Current support is Flutter 3.47.x, Android arm64 API 28+, and iOS 13.0+.

Takeaways
— flutter_patcher uses Dart's official dynamic modules, which embed a bytecode interpreter in an AOT-compiled app to load Dart bytecode at runtime.
— Patches are generated by renaming the app's own package to patch.<module>.<original> and rewriting all import paths, so the Dart VM sees only new declarations and the official no-override restriction is satisfied.
— On startup, a pre-injected entry verifies and loads the patch, then calls the patch's main function instead of the host's original main.
— Engine-launched entry points such as background isolates and push callbacks are intercepted at a low level and redirected to the patched functions.
— The Flutter framework, SDK, plugins with native code, and FFI are never included in patches; they run as machine code in the host and are called through dynamic interfaces.
— A one-line text change in the example app produces a patch of about 19 KB.
— The tool is self-hosted; the client only needs an HTTPS static file URL, and patches are signature-verified so the storage and CDN don't need to be trusted.
— If a patch crashes at startup, the app automatically falls back to the installed package and blacklists the patch; a revoked patch cannot be replayed.
— Unsupported changes (native code, FFI in patches, deleting assets, Flutter upgrades, runtime-constructed IconData) are statically checked and block patch generation.
— Current support covers Flutter 3.47.0–3.47.9, Android arm64 with API 28+, and iOS 13.0+.
— The workflow uses three CLI commands: build to create the baseline, patch to generate a diff against the baseline, and publish to sign and upload.
— Patches run in an interpreter, so computation-intensive business code will be noticeably slower; normal logic and UI interactions generally aren't affected.
— The design matches store policy by delivering only interpreted bytecode, not native executables, but review outcomes are ultimately up to each store.
Conclusions

The package-rename trick is a clean way to dodge Dart's no-override rule, but it effectively reloads all business logic under a new module path, which could complicate stack traces, analytics, or tooling that depends on package paths.

The hot-update scope is deliberately narrow: it only covers Dart-level business code and pure dependencies. Anything that touches native APIs, plugins, or FFI forces a full release, so the tool is a bug-fix mechanism, not a feature-delivery system.

Building on a precompiled engine tied to a narrow Flutter version range (3.47.x) means teams must stick to that range or wait for the project to update; that's a real maintenance burden for a library that aims to be drop-in.

Store compliance rests on the interpretation that bytecode executed by an embedded interpreter is not 'native executable code'. That line could shift if Apple or Google tighten their policies, so calling it strictly compliant is a best-effort position, not a guarantee.

Concepts & terms
Dynamic Modules
An experimental Dart capability that embeds a bytecode interpreter inside an AOT-compiled app, allowing the app to load and execute Dart bytecode at runtime without recompiling the whole binary.
AOT compilation
Ahead-of-time compilation turns Dart source into native machine code before the app ships. It makes Flutter apps fast but prevents swapping script files at runtime, which sparked the need for hot update solutions.
Baseline
The original app package that was released to users. Patches are generated as diffs against this baseline, so the toolchain knows exactly what code is already in the wild and can compute minimal changes.
Patch inline replacement
The technique of renaming the entire application code package to a new module name so that loaded bytecode adds new declarations instead of overriding existing ones, satisfying Dart's dynamic module restrictions while effectively replacing the old logic.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗