Flutter Android Specifics
On Android, a release-mode Flutter application executes its Dart business logic as native AOT machine code packaged within the ELF shared object application binary, loaded by libflutter.so. This page explains how the SDK and CLI operate specifically on Android.
1. Supported ABI Architectures (aarch64, arm, x86_64)
The Rust engine (updater/library/src/config.rs) and Dart Publisher (lib/src/cli/publisher.dart) map each OTA architecture to its canonical Android ABI folder:
OTA Architecture Identifier (arch) | Android ABI Directory (lib/<abi>/application binary) | Target Devices | CLI Behavior (-a, --arch) |
|---|---|---|---|
aarch64 | arm64-v8a | Modern 64-bit smartphones (majority of the fleet) | Included by default in --arch all |
arm | armeabi-v7a | Legacy 32-bit entry-level smartphones | Included by default in --arch all |
x86_64 | x86_64 | Android emulators on Intel PC/Mac | Supported by Rust engine (current_arch()) |
x86 | x86 | Legacy 32-bit emulators | Supported by Rust engine (current_arch()) |
Running tk-ota-push release -p android or tk-ota-push patch -p android with --arch all (default) builds the Android package in a single pass, then extracts and publishes the aarch64 (arm64-v8a) and arm (armeabi-v7a) binaries.
2. APK vs Android App Bundle (.aab) and Zero hash mismatch
During Gradle release compilation, the build pipeline creates multiple copies of application binary inside build/:
- Unstripped intermediate copies (e.g.,
build/app/intermediates/merged_native_libs/...). - Stripped release copies (
stripped_native_libs) packaged inside the final.apkor.aabarchive.
If the OTA server locked an unstripped intermediate binary while the device ran the stripped binary from the installed package, the Rust engine would reject the patch with Update rejected: hash mismatch.
To guarantee 100% hash parity, Publisher._extractLibAppFromPackage (lib/src/cli/publisher.dart) uses direct package extraction:
- The CLI decodes the final generated archive (
app-release.apkorapp-release.aab) in memory usingZipDecoder(package:archive). - It extracts the exact file:
- For an APK (
--artifact apk, default):lib/<abi>/application binary - For a Google Play App Bundle (
--artifact aab):base/lib/<abi>/application binary
- For an APK (
- If falling back to the
build/tree, the CLI gives strict priority to paths containingstripped_native_libs.
Publishing with Google Play App Bundle (.aab)
# 1. Lock the baseline release from the App Bundle (.aab)
tk-ota-push release -p android --artifact aab
# 2. Publish a patch compatible with the Google Play distributed bundle
tk-ota-push patch -p android --artifact aab -m "Fix balance display"
3. Android Native Integrity Safeguard (NativeIntegrityScanner)
On Android, an OTA patch replaces application binary (compiled Dart code), but cannot alter Java/Kotlin bytecode or the Android manifest installed on device.
NativeIntegrityScanner.scanAndroidArchive (lib/src/cli/native_integrity.dart) scans the .apk or .aab archive and computes a deterministic SHA-256 hash (nativeCodeHash) covering three categories:
- All DEX Files:
classes*.dex,base/dex/classes*.dex(compiled Java/Kotlin bytecode). - Binary Manifest:
AndroidManifest.xmlorbase/manifest/AndroidManifest.xml(permissions, activities, services). - All Native C/C++ Libraries:
lib/<arch>/*.soorbase/lib/<arch>/*.so, strictly excludingapplication binary.
During tk-ota-push patch -p android:
- If you modified only Dart code (or bumped pure Dart packages in
pubspec.lock),nativeCodeHashremains identical and the patch publishes immediately without requiring--force. - If you added an Android native plugin, changed Kotlin/Java files, or added an
AndroidManifest.xmlpermission,localNativeHash != remoteRel.nativeCodeHash: the CLI blocks publication and prompts you to publish a new Store Release (tk-ota-push release).
4. Gradle Product Flavors & applicationId Detection
FlutterProject._detectAndroidApplicationId (lib/src/cli/project.dart) automatically parses android/app/build.gradle.kts (Kotlin DSL) or android/app/build.gradle (Groovy), as well as AndroidManifest.xml:
- Extracts
applicationIdornamespacedeclared indefaultConfig. - When
--flavor <name>or--env <name>is provided, inspects the matching block inproductFlavorsorbuildTypes. - If the flavor specifies a full
applicationId, that ID is used directly. - If the flavor specifies an
applicationIdSuffix(e.g.,applicationIdSuffix ".dev"), it is appended to the baseapplicationId(e.g.,com.saramoney.retail.dev).
5. Dart Obfuscation, ProGuard / R8, and .symbols
- Dart Obfuscation (
--obfuscate): active by default inPublisher(obfuscate = true). The CLI automatically passes--obfuscateand--split-debug-info=build/ota_symbols/<version>toflutter build, archivingapp.android-arm64.symbolsandapp.android-arm.symbolsonto the OTA server. - R8 / ProGuard (Java/Kotlin): setting
minifyEnabled trueinbuild.gradleminifies Java/Kotlinclasses*.dexbytecode. The native Rust engine is embedded insidelibflutter.soand remains unaffected by R8, provided default Flutter ProGuard rules forio.flutter.**remain intact.
6. Required Android Permissions
Only android.permission.INTERNET is required inside android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
Patch downloads and storage take place entirely within the app's private internal storage (/data/user/0/<package>/files and code_cache/downloads); neither READ_EXTERNAL_STORAGE nor WRITE_EXTERNAL_STORAGE is needed.