跪拜 Guibai
← Back to the summary

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

flutter_patcher 1.0.0: A New Flutter Hot Update Solution for Both Platforms Based on Dart's Official Dynamic Modules

Open source, no code changes, no SDK changes, self-hosted, works on both iOS and Android, and designed to meet App Store and Google Play policy requirements.


1. Why reinvent hot update?

Flutter developers have all been through it: a small bug hits production, the fix is one line, but you have to repackage, resubmit, wait for review, and wait for users to upgrade. Fast case: a day or two. Slow case: up to a week.

Hot update is the standard solution to this problem, but on Flutter it keeps hitting three practical obstacles:

1. The community lacks a mature open-source solution. Flutter release packages are AOT-compiled machine code, so you can't swap in a script file like you do with JS. Past community exploration falls into two categories: writing your own Dart interpreter or converting pages to DSL for delivery – both restrict how you write code and fragment the experience – or replacing the .so dynamic library directly, which only works on Android, not iOS, and is a violation on Google Play.

2. Shorebird has a high adoption barrier. Shorebird is currently the best-known commercial solution and is technically strong, but integration often runs into obstacles: its core service is tied to the official cloud and billed per patch installation, so costs climb as user numbers grow; it doesn't support private deployment – patches and data must pass through third-party servers, which many companies can't pass through internal compliance; and network stability in China is a real issue.

3. App store review red lines. This point is the easiest to overlook and the most fatal:

So a truly production-ready Flutter hot update must run patches in an "interpreted" manner, and at the same time, disrupt developers' existing coding habits as little as possible.

Based on this thinking, I built flutter_patcher.

2. What is flutter_patcher

flutter_patcher is an open-source, self-hostable Flutter hot update solution that complies with both stores' policies:

3. How does it work?

Below, the three questions everyone cares about most: what runs it, how old code gets replaced, and why stores allow it.

1. Foundation: Dart's official dynamic modules

The Dart and Flutter teams have recently been working on an experimental capability called Dynamic Modules: embedding a bytecode interpreter inside an AOT-compiled app so the app can load and execute Dart bytecode at runtime. flutter_patcher builds on top of this official capability.

This capability is not yet shipped with the default SDK and requires you to compile the engine from source. flutter_patcher handles that step for you: it provides a precompiled engine, built from the official source with dynamic modules enabled, plus a few low-level changes (fixing a VM bytecode read exception, enabling resource reading from within patches, and intercepting engine entry points). You don't need to compile anything yourself to integrate.

2. Core challenge: the official restriction only allows adding, not replacing

The official dynamic modules have a hard restriction: loaded code can only add new declarations, and cannot override declarations that already exist in the app. But the whole point of hot updates is to "replace old code". How do we get around that?

flutter_patcher does it by rewriting the package name and switching the entry point:

  1. Rename the whole package, recompile: When generating a patch, the tool copies your application's own code package in full, renames it to patch.<module number>.<original package name>, rewrites the import paths inside, and finally compiles it into a bytecode module. To the Dart VM, all loaded declarations are new; no existing code is overridden, which fully complies with the official restriction. The tool also performs static checks: if a patch still references an unrenamed old package path, it stops and raises an error.

  2. Start from the patch's main: When you run build to create the baseline package, the framework injects a pre-start entry that runs before your main. On a cold start, it verifies and loads the patch, then directly calls the main function inside the patch. The old business code in the host package is no longer invoked in that run.

  3. Intercept other entry points: For entry points launched directly by the engine, such as background isolates and push callbacks, the precompiled engine intercepts them at a low level and automatically redirects calls to the corresponding new functions in the patch.

  4. Share the underlying framework: The Flutter framework layer, the SDK, and plugins that contain native code, FFI, and packages that depend on them, are not included in the patch. They continue to run as the host package's machine code, and the patch calls them through dynamic interfaces.

On patch size control, the framework uses two modes: normally it only packages your application's own business code (lean mode); when it detects changes to pure Dart dependencies, it automatically upgrades to include unlocked pure Dart dependencies as well (whole mode). Because the framework and native parts are stripped out, patch sizes are very small – in the example app, a one-line text change produces a patch of about 19 KB.

3. Why is it within policy

Both major app stores draw their compliance line at not delivering native executable code, but they allow code that is interpreted. flutter_patcher's bytecode patch runs in the app's built-in interpreter. It doesn't generate new .so or .dex files, and it doesn't ask the system for executable memory. This is the same category as many apps that deliver JS or games that ship Lua scripts.

Of course, technical compliance doesn't mean you can use it freely: hot updates should only be used to fix production bugs and make small adjustments. They must not be used to drastically change the app's core functionality or bypass review. That boundary is the developer's responsibility. A final note: the review result is ultimately decided by the store, and no hot update scheme can make a 100% guarantee. What we can do is keep the technical implementation within the explicitly allowed scope of the policy.

4. Performance and boundaries

These unsupported changes are all statically checked when generating the patch; if something goes out of bounds, the tool reports an error, avoiding the shipment of a broken patch.

Current environment requirements: supports Flutter 3.47.0–3.47.9 (generating a patch requires the same Flutter version used for the baseline package); Android requires arm64 architecture and API 28 or higher; iOS requires 13.0 or higher. Devices that don't meet the conditions simply run the original package without loading the patch.

4. Getting started in three steps

1. Add dependency and initialize

flutter pub add flutter_patcher
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterPatcher.init();
  runApp(const MyApp());
}

// Check for updates at an appropriate time; update URL is configured in flutter_patcher.yaml's url field
final check = await FlutterPatcher.checkUpdate();
if (check.hasUpdate) {
  await FlutterPatcher.applyPatch(check.patch!);
}

2. Generate keys, build the baseline installable

dart run flutter_patcher keys init       # Run once: generates keys; commit the generated flutter_patcher.yaml to Git
dart run flutter_patcher build android   # or build ios (run on an Apple Silicon Mac)

Use the package produced by build for release. This command records the current version as the baseline (Baseline) locally; later patches are generated as diffs against this baseline.

3. Fix the bug, generate and publish the patch

dart run flutter_patcher patch android   # Generate diff patch against baseline
dart run flutter_patcher publish         # Sign and upload to storage

The publish command can write artifacts directly to a local directory, or automatically upload to S3 and compatible services (such as Cloudflare R2); if you use other cloud storage, upload the artifacts manually.

5. Finally

flutter_patcher 1.0.0 does one thing: if a bug is found after release, you can fix it without shipping a new version.

It uses Dart's official dynamic modules. Custom engine compilation, package renaming, entry switching, patch signing, and automatic rollback after startup crashes are all handled by the toolchain. Fixing a daily bug only needs three commands: build to package and release, patch to generate a patch, and publish to release it.

If this tool helps you, feel free to give it a ⭐️ on GitHub. If you run into problems or have ideas, please open an Issue or leave a comment.