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:
- Run
shorebird release android
- Upload AAB to Play Store (Internal Testing track)
- Install the app via Play Store — all deferred components load correctly ✅
- Make any change to base app code (e.g., change a string in
lib/main.dart)
- Run
shorebird patch android
- Open the app — Shorebird patch is applied successfully (Shorebird updater reports patch installed)
- Trigger any deferred component load (e.g., navigate to a screen that calls
loadLibrary())
- 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:core → feature-delivery:2.x migration). The error is reproducible on both physical devices and emulators running Android 10+.
Summary
After applying any
shorebird patch android— even patches that only modify base app code — all deferred components fail to load with:This makes Shorebird patches completely incompatible with Flutter Deferred Components that use Play Feature Delivery split APKs.
Environment
shorebird --version1.6.98flutter --version3.41.9dart --version3.11.5flutter.deferred-componentsin pubspec.yaml)Steps to Reproduce
Setup:
pubspec.yamldeclares deferred components:Dart code uses deferred imports:
Reproduce:
shorebird release androidlib/main.dart)shorebird patch androidloadLibrary())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
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
.sofiles — one per loading unit: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:What Shorebird patch does
shorebird release androidcompiles all loading units together → all.sofiles share fingerprintV1. These are uploaded to the Play Store and distributed as split APKs via Play Feature Delivery.shorebird patch androidrecompiles and patches onlylibapp.so(the main loading unit). The patchedlibapp.sonow has fingerprintV2.However, the deferred component
.sofiles (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 fingerprintV1.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:
Any Flutter app that uses
flutter.deferred-componentsinpubspec.yamlcannot useshorebird patchwithout breaking all deferred functionality. The only workaround today is to avoid patching altogether and useshorebird releasefor 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.sofiles would need to be distributed alongside or instead of the Play Feature Delivery split APKs.Challenges:
.sofiles are currently distributed via Play Feature Delivery, outside Shorebird's controlOption 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_pushthat indicates a patch is active, so apps can gracefully degrade (e.g., skiploadLibrary()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
dart/runtime/vm/dart.cc—DartVM::Initfingerprint verificationAdditional Notes
This issue was discovered in a Flutter monorepo using Play Feature Delivery with three deferred components and a custom
DeferredComponentManager(required due to theplay:core→feature-delivery:2.xmigration). The error is reproducible on both physical devices and emulators running Android 10+.