Table of Contents
Overview
Starting with Android 15, new devices such as Pixel 8 and above, and the Galaxy S26 began using a 16KB memory page size instead of the traditional 4KB. If a native library (.so) bundled inside the AAB (Android App Bundle) does not satisfy 16KB alignment, the Play Store marks the app as incompatible for those devices, meaning it will not appear in search results.
This post summarizes the work I did to make a Flutter project compatible with 16KB page sizes.
- Explicitly declare alignment preservation in the build configuration
- Build a static gate that automatically verifies alignment in AAB artifacts
- Verify runtime behavior on a 16KB emulator
Why 16KB Page Size Matters
The Linux kernel manages virtual memory in page units. On devices with 4KB pages, an ELF (Executable and Linkable Format) binary’s LOAD segments only needed 4KB (0x1000) alignment for memory mapping to succeed.
On 16KB page devices, LOAD segments must satisfy 16KB (0x4000) alignment. Calling dlopen() on an unaligned .so produces a runtime error like this:
dlopen failed: "libxxx.so" is not page-aligned
The Play Store checks this alignment at AAB upload time. AGP 8.5+ emits warnings for unaligned libraries, and AGP 8.7+ fails the build outright. Even so, pre-built plugin .so files often fail to satisfy alignment, so it is necessary to verify the artifact directly in CI.
Build Configuration Changes
Check AGP Version
First, verify that the Android Gradle Plugin version is 8.5 or higher in android/settings.gradle.kts (or android/build.gradle).
plugins {
id("com.android.application") version "8.7.0" apply false
// ...
}
If the version is below 8.5, you need to upgrade. AGP 8.5+ enforces 16KB alignment at build time, so if any unaligned plugin is included, the build will fail and report which plugin is the cause.
Explicitly Declare useLegacyPackaging
Add an explicit packaging declaration to the android { ... } block in android/app/build.gradle.kts.
android {
// ...
// 16KB page-size compatibility — keep extractNativeLibs=false so AGP/Android packs
// .so files uncompressed and preserves their LOAD-segment alignment for Galaxy S26
// and other 16KB-page devices. AGP 8.5+ enforces 16KB alignment at packaging time.
packaging {
jniLibs {
useLegacyPackaging = false
}
}
}
useLegacyPackaging = false means that .so files are packaged uncompressed inside the APK/AAB. Without compression, alignment is preserved at the ZIP entry level, and the runtime can map the files directly with mmap().
The default has been false since AGP 8.0+, but we declare it explicitly to prevent regressions. Some plugins inject extractNativeLibs="true" through manifest merging, and relying on the default can silently regress to compressed packaging.
Building a Static Verification Gate
Build configuration alone is not enough. Build a script that unpacks the AAB artifact in CI and directly verifies alignment, blocking regressions in advance.
Verification has two stages.
- Stage A: Verify ELF LOAD segment alignment of
base/lib/arm64-v8a/*.soandbase/lib/x86_64/*.soinside the AAB (ZIP) - Stage B: Build a universal APK with bundletool and verify 16KB alignment at the ZIP entry level
Check .so LOAD Alignment with llvm-readobj
llvm-readobj parses the ELF header and prints the Alignment value of LOAD segments. It ships with the Android NDK, so no separate installation is needed.
#!/usr/bin/env bash
# verify_16kb_alignment.sh (summary)
AAB_PATH="${1:-build/app/outputs/bundle/release/app-release.aab}"
WORK_DIR="$(mktemp -d -t verify16kb_XXXXXX)"
trap 'rm -rf "${WORK_DIR}"' EXIT
# Unpack the AAB (ZIP)
unzip -q "${AAB_PATH}" -d "${WORK_DIR}/aab"
ISSUES=0
for abi in arm64-v8a x86_64; do
for so in "${WORK_DIR}/aab/base/lib/${abi}"/*.so; do
# LOAD segment Alignment must be >= 16384 (0x4000)
align=$(llvm-readobj -l "${so}" \
| awk '/Type: PT_LOAD/{found=1} found && /Alignment:/{print $2; exit}')
if [ "${align}" -lt 16384 ]; then
echo "FAIL: ${abi}/$(basename ${so}) p_align=${align} (< 0x4000)"
ISSUES=$((ISSUES + 1))
else
echo "OK: ${abi}/$(basename ${so}) aligned p_align=${align}"
fi
done
done
if [ "${ISSUES}" -eq 0 ]; then
echo "RESULT: PASS"
exit 0
else
echo "RESULT: FAIL (${ISSUES} issues)"
exit 1
fi
The key check is that the Alignment value of the PT_LOAD segments extracted by llvm-readobj -l is at least 16384 (0x4000). Both arm64-v8a and x86_64 ABIs are checked. 32-bit ABIs (armeabi-v7a, x86) are excluded because 16KB page devices do not support them.
Check Universal APK ZIP Alignment with zipalign
In addition to ELF alignment, also verify the alignment of ZIP entries in the APK itself. Build a universal APK with bundletool and inspect it with zipalign -P 16.
# Build the universal APK
bundletool build-apks \
--bundle="${AAB_PATH}" \
--output="${WORK_DIR}/app-release.apks" \
--mode=universal \
--ks=android/key.jks --ks-pass=pass:... --ks-key-alias=...
# Extract universal.apk from the .apks
unzip -j "${WORK_DIR}/app-release.apks" universal.apk -d "${WORK_DIR}"
# Verify 16KB ZIP alignment (-P 16 = 16KB page size, -c = check, -v = verbose)
zipalign -c -v -P 16 4 "${WORK_DIR}/universal.apk"
-P 16 tells zipalign to check alignment based on a 16KB page size, and 4 is the ZIP entry alignment unit for regular files. A non-zero exit code from zipalign means at least one entry is misaligned.
In the CI workflow, combine these two stages into a post-build verification step for release builds.
# .github/workflows/release_android.yml (summary)
- name: Build release AAB
run: flutter build appbundle --release
- name: Verify 16KB alignment
run: bash scripts/verify_16kb_alignment.sh build/app/outputs/bundle/release/app-release.aab
If FAIL is reported, track upstream PRs for the offending plugin or work around it with a temporary fork or alternative plugin. Either way, the key point is that release must not proceed while CI is failing.
Runtime Verification on a 16KB Emulator
Even after the static gate passes, 16KB alignment can still break at JNI call time (for example, when an ad SDK loads .so files dynamically). Runtime behavior cannot be caught statically, so run the app on a 16KB emulator once before each release.
Install the 16KB System Image
The SDK package name of a 16KB page system image uses the ..._ps16k suffix. For validating payment SDKs such as in_app_purchase, prefer the Play Store-bundled variant (...playstore_ps16k).
# Run once if licenses are not yet accepted
"${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses
# Install the 16KB system image (API 36 / Apple Silicon example)
"${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --install \
"system-images;android-36;google_apis_playstore_ps16k;arm64-v8a"
# Create the AVD
"${ANDROID_HOME}/cmdline-tools/latest/bin/avdmanager" create avd \
--name "Pixel_8_Pro_16KB_API36" \
--package "system-images;android-36;google_apis_playstore_ps16k;arm64-v8a" \
--device "pixel_8_pro" \
--force
Choose the arm64-v8a ABI on Apple Silicon Macs and x86_64 on Intel hosts.
Verify Page Size After Boot
# Boot
"${ANDROID_HOME}/emulator/emulator" -avd Pixel_8_Pro_16KB_API36 &
# Verify page size — must print 16384
adb shell getconf PAGE_SIZE
If 4096 is printed instead of 16384, you booted a regular 4KB AVD. Recheck that the system image package name contains _ps16k.
Install Release AAB and Verify Behavior
Build a universal APK with bundletool and install it on the 16KB emulator.
# Build the universal APK (signed with release keystore)
bundletool build-apks \
--bundle=build/app/outputs/bundle/release/app-release.aab \
--output=build/app/outputs/bundle/release/app-release.apks \
--mode=universal \
--ks=android/key.jks --ks-pass=pass:... --ks-key-alias=...
# Install
bundletool install-apks --apks=build/app/outputs/bundle/release/app-release.apks
After installation, verify the following checklist.
- App boots (splash → home screen)
- DB queries work (sqlite3 native library)
- TTS playback (
flutter_tts) and speech recognition (speech_to_text) - Ad loading (
google_mobile_ads) - Product list loads on payment screen (
in_app_purchase) - Firebase Analytics / Crashlytics / Performance initialization
Monitor Logcat for crashes or not page-aligned messages.
adb logcat -v time | grep -E "(FATAL|ANR|page-size|JNI|ELF)" --color=never
If no FATAL EXCEPTION, ANR, dlopen failed, or not page-aligned messages appear over 5+ minutes of normal use, treat it as a pass.
Play Console Pre-validation
You can verify in advance whether the build will pass the Play Console without performing an actual release. Sending the AAB through Fastlane’s supply --validate-only path validates the following items without changing the release or track.
- AAB signature (keystore alias consistency)
applicationIdconsistencyversionCodecollision check- Play-side 16KB alignment enforcement (double-checking the static gate)
- Track / metadata consistency
# android/fastlane/Fastfile
lane :validate_release do
upload_to_play_store(
aab: "../build/app/outputs/bundle/release/app-release.aab",
track: "production",
validate_only: true,
)
end
cd android && bundle exec fastlane validate_release
On success, Successfully finished the upload to Google Play is printed and no actual upload occurs. On failure, it exits with a clear reason (e.g., versionCode 1234 has already been used). This is useful for catching errors that the release workflow would encounter when a tag is pushed.
Summary
To summarize the work required to make a Flutter app properly listed on the Play Store for 16KB page devices such as the Galaxy S26:
Summary:
- Use AGP 8.5+ and explicitly declare
packaging.jniLibs.useLegacyPackaging = falseinbuild.gradle.ktsto prevent regressions - Do not rely solely on the build configuration. Build a CI gate that directly inspects ELF LOAD alignment and ZIP entry alignment of AAB artifacts
- Use
llvm-readobj -lto confirm PT_LOAD segment Alignment ≥ 0x4000 - Use bundletool +
zipalign -P 16to verify the universal APK
- Use
- Runtime behavior (JNI / ads / billing / Firebase) cannot be caught statically, so perform one manual verification on a 16KB emulator (
_ps16ksystem image) before release - Play Console enforcement can be dry-run in advance with Fastlane
validate_only
The trend of new devices adopting a 16KB page size will continue for some time. Once you set up the three layers — build configuration, static gate, and runtime SOP — you can keep releases stable each time a new device is launched.
Related Resources
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.