Dynamic Flutter Package Splitting and Loading for iOS and Android
This article details the motivation, design, implementation, and results of a dynamic Flutter package splitting solution that extracts data and resource segments on iOS and separates native libraries and assets on Android, enabling on‑demand download and reducing overall app size.
Background – JD.com’s “to‑home” app replaced 19 activity landing pages with Flutter, causing the app bundle to exceed the 100 MB size limit, with Flutter modules accounting for about 20 % of the total package. To keep the app lightweight, a dynamic Flutter package delivery mechanism was explored.
iOS implementation
The iOS side integrates several frameworks (App.framework, Flutter.framework, hybird_router.framework, and plugin frameworks). App.framework contains four snapshot sections: kDartVmSnapshotData, kDartVmSnapshotInstructions, kDartIsolateSnapshotData, and kDartIsolateSnapshotInstructions. Because iOS forbids marking memory pages executable at runtime, the VM and isolate instruction segments must be pre‑packed, but Flutter’s engine provides hooks to load these segments externally, allowing them to be split out for dynamic delivery.
The build process invokes flutter_tools.snapshot (implemented in gen_snapshot.cc) where Dart_CreateSnapshot() writes the snapshot. By modifying AssemblyImageWriter::WriteText and WriteTextToLocalFile, the VM and isolate data are written to separate files ( VmSnapshotData.S and IsolateSnapshotData.S) for each architecture (arm64/armv7). Resource files are moved after the product is generated.
void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
// ...
#if defined(TARGET_OS_MACOS_IOS)
WriteTextToLocalFile(clustered_stream, vm);
#else
// original write logic
#endif
}
void AssemblyImageWriter::WriteTextToLocalFile(WriteStream* clustered_stream, bool vm){
#if defined(TARGET_OS_MACOS_IOS)
auto OpenFile = [](const char* filename){
Syslog::Print("open file : %s
", filename);
bin::File* file = bin::File::Open(NULL, filename, bin::File::kWriteTruncate);
if (file == NULL) {
Syslog::PrintErr("Error: Unable to write file: %s
", filename);
Dart_ExitScope();
Dart_ShutdownIsolate(); exit(255);
}
return file;
};
// ... write logic for arm64/armv7 ...
#endif
}During engine initialization, the paths for the VM and isolate snapshots are read from FlutterDartProject. By resetting settings.ios_vm_snapshot_data_path and settings.ios_isolate_snapshot_data_path to the downloaded files, the engine loads the split data at runtime.
Android implementation
On Android, three deliverable parts are identified: libapp.so, libflutter.so, and Flutter_assets. Gradle tasks are hooked to copy these artifacts to a temporary directory and remove them from the original output. The custom tasks are chained as follows:
def FlutterUploadTask = tasks.findByName('uploadFlutterToJdCloud')
def FlutterSignAndZipTask = tasks.findByName('FlutterSignAndZip')
def FlutterStripSoTask = tasks.findByName('FlutterStripSo')
FlutterStripSoTask.finalizedBy FlutterSignAndZipTask
FlutterSignAndZipTask.finalizedBy FlutterUploadTaskA Gradle listener registers the following hook:
tasks.whenTaskAdded { task ->
if (project.IS_Flutter_DOWNLOAD) {
if (task.name.startsWith('merge') && task.name.endsWith('Assets')) {
task.finalizedBy('FlutterStripAssets')
} else if (task.name.startsWith('transformNativeLibsWithMergeJniLibsFor')) {
task.finalizedBy('FlutterStripSo')
}
}
}After signing and zipping, the split packages are uploaded to JD Cloud, which returns three URLs in JSON. The client writes these URLs into the asset directory and downloads them on first launch.
Dynamic loading on Android
The engine loads libflutter.so via System.loadLibrary("flutter"). To load a custom libapp.so, the code reflects into the class loader’s nativeLibraryDirectories and inserts the path of the downloaded library before initialization. The resource bundle is loaded by inserting a DirectoryAssetBundle at the head of the asset manager’s provider list in RunBundleAndSnapshotFromLibrary.
Business scenarios
At app startup, the client checks remote configuration to decide whether to download the split Flutter artifacts. If download fails, a fallback Web page is shown while the download is retried. This prevents blocking the user experience.
Issues and mitigations
iOS: Engine version compatibility and missing macOS SDK caused build failures; the solution was to align the Flutter engine version with the CI environment.
Android: UnsatisfiedLinkError for libflutter.so occurred because native library directories were set before the split libraries were downloaded; the fix was to pre‑populate an empty native path during application initialization.
Results
After applying dynamic delivery, iOS split data size is 19.6 MB (23.9 % of the total 56.8 MB Flutter module). Android achieves over 90 % split, reducing the APK size by roughly 9 MB.
Future plans
iOS: Investigate incremental versioning and code‑size impact analysis.
Android: Further granulate split packages and add resumable download support to improve speed and success rate.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Dada Group Technology
Sharing insights and experiences from Dada Group's R&D department on product refinement and technology advancement, connecting with fellow geeks to exchange ideas and grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
