Skip to main content

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 DevicesCLI Behavior (-a, --arch)
aarch64arm64-v8aModern 64-bit smartphones (majority of the fleet)Included by default in --arch all
armarmeabi-v7aLegacy 32-bit entry-level smartphonesIncluded by default in --arch all
x86_64x86_64Android emulators on Intel PC/MacSupported by Rust engine (current_arch())
x86x86Legacy 32-bit emulatorsSupported 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 .apk or .aab archive.

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:

  1. The CLI decodes the final generated archive (app-release.apk or app-release.aab) in memory using ZipDecoder (package:archive).
  2. 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
  3. If falling back to the build/ tree, the CLI gives strict priority to paths containing stripped_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:

  1. All DEX Files: classes*.dex, base/dex/classes*.dex (compiled Java/Kotlin bytecode).
  2. Binary Manifest: AndroidManifest.xml or base/manifest/AndroidManifest.xml (permissions, activities, services).
  3. All Native C/C++ Libraries: lib/<arch>/*.so or base/lib/<arch>/*.so, strictly excluding application binary.

During tk-ota-push patch -p android:

  • If you modified only Dart code (or bumped pure Dart packages in pubspec.lock), nativeCodeHash remains identical and the patch publishes immediately without requiring --force.
  • If you added an Android native plugin, changed Kotlin/Java files, or added an AndroidManifest.xml permission, 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 applicationId or namespace declared in defaultConfig.
  • When --flavor <name> or --env <name> is provided, inspects the matching block in productFlavors or buildTypes.
  • 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 base applicationId (e.g., com.saramoney.retail.dev).

5. Dart Obfuscation, ProGuard / R8, and .symbols​

  • Dart Obfuscation (--obfuscate): active by default in Publisher (obfuscate = true). The CLI automatically passes --obfuscate and --split-debug-info=build/ota_symbols/<version> to flutter build, archiving app.android-arm64.symbols and app.android-arm.symbols onto the OTA server.
  • R8 / ProGuard (Java/Kotlin): setting minifyEnabled true in build.gradle minifies Java/Kotlin classes*.dex bytecode. The native Rust engine is embedded inside libflutter.so and remains unaffected by R8, provided default Flutter ProGuard rules for io.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.