Skip to content

fix: Shorebird patch breaks Flutter Deferred Components (Play Feature Delivery) — DeferredLoadException: program fingerprint mismatch #3761

Description

@Abdan009

Summary

After applying any shorebird patch android — even patches that only modify base app code — all deferred components fail to load with:

DeferredLoadException: Deferred loading unit is from different program than the main loading unit

This makes Shorebird patches completely incompatible with Flutter Deferred Components that use Play Feature Delivery split APKs.


Environment

Version
Shorebird CLI shorebird --version 1.6.98
Flutter SDK flutter --version 3.41.9
Dart SDK dart --version 3.11.5
Android Gradle Plugin 8.x
Platform Android
Delivery mechanism Play Feature Delivery (split APKs via flutter.deferred-components in pubspec.yaml)

Steps to Reproduce

Setup:

pubspec.yaml declares deferred components:

flutter:
  deferred-components:
    - name: presence_feature
      libraries:
        - package:presence/presence.dart
    - name: calculator_feature
      libraries:
        - package:calculator/calculator.dart

Dart code uses deferred imports:

import 'package:presence/presence.dart' deferred as presence;

// Triggered on user action:
await presence.loadLibrary();

Reproduce:

  1. Run shorebird release android
  2. Upload AAB to Play Store (Internal Testing track)
  3. Install the app via Play Store — all deferred components load correctly ✅
  4. Make any change to base app code (e.g., change a string in lib/main.dart)
  5. Run shorebird patch android
  6. Open the app — Shorebird patch is applied successfully (Shorebird updater reports patch installed)
  7. Trigger any deferred component load (e.g., navigate to a screen that calls loadLibrary())
  8. App crashes / throws exception ❌

Expected Behavior

Shorebird patch is applied to the main loading unit. Deferred components that were not modified continue to load correctly, since their code has not changed.


Actual Behavior

DeferredLoadException: Deferred loading unit is from different program than the main loading unit

All deferred components fail to load, regardless of whether their code was modified in the patch.


Root Cause Analysis

How Dart AOT loading unit fingerprinting works

When Dart AOT compiles a Flutter app with deferred components, it produces multiple .so files — one per loading unit:

libapp.so      ← main loading unit (loading unit 0)
libapp-2.so    ← deferred component A
libapp-3.so    ← deferred component B

At compile time, Dart embeds a program fingerprint (a hash derived from the entire compiled program) into every loading unit .so. This fingerprint is used at runtime to verify that all loading units were compiled from the same Dart program:

// Dart runtime check (simplified):
if (deferred_unit.program_fingerprint != main_unit.program_fingerprint) {
  throw DeferredLoadException('Deferred loading unit is from different program...');
}

What Shorebird patch does

shorebird release android compiles all loading units together → all .so files share fingerprint V1. These are uploaded to the Play Store and distributed as split APKs via Play Feature Delivery.

shorebird patch android recompiles and patches only libapp.so (the main loading unit). The patched libapp.so now has fingerprint V2.

However, the deferred component .so files (libapp-2.so, libapp-3.so, etc.) are bundled inside Play Feature Delivery split APKs managed by the Play Store. Shorebird has no mechanism to patch these files. They remain at fingerprint V1.

After shorebird patch:

  libapp.so (Shorebird-patched) → fingerprint: V2
  libapp-3.so (Play Store split) → fingerprint: V1

  V2 ≠ V1 → DeferredLoadException on every loadLibrary() call

Why base-app-only changes still break deferred components

The program fingerprint is computed from the entire compiled program — not just the main loading unit. Any code change, even a single character in lib/main.dart, produces a different fingerprint. This means any Shorebird patch, regardless of what was changed, will cause a fingerprint mismatch with existing deferred component split APKs.


Impact

This is a complete incompatibility between two first-class Flutter features:

  • Flutter Deferred Components (officially supported, documented at flutter.dev)
  • Shorebird code push

Any Flutter app that uses flutter.deferred-components in pubspec.yaml cannot use shorebird patch without breaking all deferred functionality. The only workaround today is to avoid patching altogether and use shorebird release for every update, which eliminates Shorebird's core value proposition.


Potential Solutions

For the Shorebird team's consideration:

Option A — Patch all loading units

Extend the Shorebird patch mechanism to also generate and distribute patches for each deferred loading unit .so. The patched deferred .so files would need to be distributed alongside or instead of the Play Feature Delivery split APKs.

Challenges:

  • Deferred .so files are currently distributed via Play Feature Delivery, outside Shorebird's control
  • Would require a parallel delivery mechanism for patched deferred loading units

Option B — Preserve fingerprint across patches

Investigate whether Dart's fingerprint check can be made version-aware — i.e., the deferred loading unit declares a minimum compatible main unit fingerprint range rather than a single exact fingerprint. This would require changes on the Dart VM side.

Option C — Expose compatibility flag

Expose an API in shorebird_code_push that indicates a patch is active, so apps can gracefully degrade (e.g., skip loadLibrary() and show an "update required" screen) instead of crashing.

This is a partial mitigation rather than a fix, but it would prevent force closes while a full solution is developed.


References


Additional Notes

This issue was discovered in a Flutter monorepo using Play Feature Delivery with three deferred components and a custom DeferredComponentManager (required due to the play:corefeature-delivery:2.x migration). The error is reproducible on both physical devices and emulators running Android 10+.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions