Android devices have used 4 KB memory pages for as long as the platform has existed. Newer devices can run with 16 KB pages, and Google Play now requires apps to be ready for them.
For an app with any native code, including native code hidden inside a library, it can be a hard block on releasing updates.
The trap
The trap is assuming the app has no native code because you never wrote C++.
Machine learning runtimes, video and audio libraries, database engines, image codecs and some analytics SDKs all ship .so files. If one of them was built for 4 KB pages only, your app is not compliant. The warning shows up in Play Console on someone else's code.
The requirement
From "Support 16 KB page sizes" on developer.android.com (checked October 2026): all apps targeting Android 15 (API level 35) and higher must support 16 KB memory page sizes on 64-bit devices on Google Play.
Starting February 1, 2027, if your app updates don't support 16 KB page sizes, you won't be able to release them. Since the target API requirement is already API 36, every app that ships updates is in scope.
Only native code can be out of line. A Kotlin and Java only app, including all its libraries, already supports 16 KB devices.
What "aligned" means
A shared library is loaded into memory in segments. On a 16 KB device each segment has to start on a 16 KB boundary.
A library linked for 4 KB pages has segments that start on 4 KB boundaries. On a 16 KB device it can fail to load or crash. The fix is not in your Kotlin. It is in how the .so is linked and how it is stored in the APK.
Quick look first: Analyze APK
In Android Studio, open Build > Analyze APK and pick your release APK. Look in the lib folder.
If it is missing or empty, the app has no native code and you are done. If there are .so files, the Alignment column shows a warning for each file with an alignment problem. Android Studio and lint also flag prebuilt libraries that are not 16 KB aligned.
The commands below are what a CI job should run.
Command 1: llvm-objdump on every library
Unpack the APK and print the LOAD segments of each library:
# 1. Unpack the APK
unzip -q app-release.apk -d /tmp/apk_out
# 2. Each LOAD segment must show align 2**14 (16 KB) or more
NDK_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
for so in /tmp/apk_out/lib/arm64-v8a/*.so /tmp/apk_out/lib/x86_64/*.so; do
[ -f "$so" ] || continue
echo "$so"
"$NDK_BIN/llvm-objdump" -p "$so" | grep LOAD
done
Any LOAD line with align 2**13, 2**12 or lower is a library that needs rebuilding or replacing.
Command 2: zipalign on the APK
The libraries also have to be stored correctly inside the package. Check the zip alignment with Build-Tools 35.0.0 or newer:
# 3. Zip alignment of the APK itself (Build-Tools 35.0.0 or newer)
"$ANDROID_HOME"/build-tools/35.0.0/zipalign -v -c -P 16 4 app-release.apk
It ends with "Verification successful" when every library is stored correctly.
Command 3: bundletool for an App Bundle
For an App Bundle, check what the bundle asks for:
bundletool dump config --bundle=app-release.aab | grep alignment
PAGE_ALIGNMENT_16K means the bundle asks for 16 KB zip alignment. PAGE_ALIGNMENT_4K means APKs built from it will have 4 KB aligned .so files.
Command 4: confirm a real 16 KB environment
Use an Android Emulator system image with 16 KB page size (the guide lists the available images and the developer option on supported devices). Install the release build, and confirm the environment first:
adb shell getconf PAGE_SIZE # should print 16384
Then exercise every feature that touches native code: the ML model, the video export, the database. A library that loaded fine on 4 KB pages and crashes here is exactly what this step is for.
Fixing the build
The guide's shortcut: with AGP 8.5.1 or higher, NDK r28 or higher and 16 KB compatible prebuilt dependencies, your app is 16 KB compatible by default.
Packaging. 16 KB devices need uncompressed shared libraries on a 16 KB zip-aligned boundary. The guide's fix is to upgrade to AGP 8.5.1 or higher. Keep AGP current and keep uncompressed packaging.
Your own C and C++ code. NDK r28 and later compile 16 KB aligned by default. Pin the version in Gradle so every machine uses the same one:
android {
// NDK r28+ produces 16 KB aligned libraries by default
ndkVersion = "28.2.13676358"
}
Stuck on NDK r27 or older? Add the linker flags from the guide. With CMake:
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"-Wl,-z,max-page-size=16384"
"-Wl,-z,common-page-size=16384")
And with ndk-build, in Android.mk:
LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384
Prebuilt libraries from someone else. You cannot relink a binary you do not build. Update the dependency to a version whose release notes say it supports 16 KB pages, or replace it.
A case note
The author ships Android apps on Google Play as Hartmann Studios. One of them, CaptionBurn (chartmann1590/captionburn), burns subtitles into video on the device and ships native code for it. Its app/build.gradle.kts pins ndkVersion = "28.2.13676358" with the comment "NDK r28+ produces 16 KB–aligned native libs by default (16 KB page size support)". That NDK is also the default NDK listed for AGP 9.4.
Watch out for these
If you cannot upgrade AGP to 8.5.1 or higher, the guide's alternative is to have Gradle compress the shared libraries (packaging { jniLibs { useLegacyPackaging = true } }). The app then takes more space once installed.
The guide also warns that AGP 8.3 to 8.5 align by default but bundletool does not zipalign APKs. An app can work locally and still fail to install when Play builds it from the bundle.
Compression deals with packaging only. It does not fix a library whose ELF segments are 4 KB aligned.
Native code that hard-codes the page size can still break after the libraries are aligned:
- Remove code that relies on the
PAGE_SIZEconstant or assumes pages are 4096 bytes. Usegetpagesize()orsysconf(_SC_PAGESIZE)instead. - Review
mmap()calls and other APIs that need page-aligned arguments. - Check custom memory allocators, if you have any.
Checklist
- You know whether the release APK contains any
.sofiles, including from libraries. - AGP 8.5.1 or newer and NDK r28 or newer, pinned with
ndkVersion. - Every arm64-v8a and x86_64 library shows LOAD alignment
2**14or higher. -
zipalign -c -P 16reports "Verification successful". - No native code assumes a 4096-byte page.
- The app is tested on a 16 KB emulator image (
getconf PAGE_SIZE= 16384). - Done before February 1, 2027.
Run the zipalign and objdump checks on every release build, and fail the job if any LOAD segment is below 2**14.
This is adapted from my book Pass Google Play Review (Android the Right Way, Book 3). The free sample is here and the full book is here.
Drafted with AI help and edited by the author; based on his book.
Top comments (2)
Your title is exactly right for Flutter.
A Flutter APK always ships native code: libflutter.so (the engine) and libapp.so (your Dart code, compiled ahead of time).
I ran the check on a release build today. Both come out with LOAD alignment 2**16, so the Flutter side is fine on a current SDK.
The surprise was the other two .so files: libdartjni.so, pulled in by a plugin through package:jni, and libdatastore_shared_counter.so, which ships with AndroidX DataStore.
DataStore is a Kotlin library, and there’s no C++ anywhere in our code.
Both were 16 KB aligned and stored uncompressed, but it’s a perfect example of your point: I would never have guessed DataStore carries a native library.
Thanks for running it on a real release build, and the DataStore one is a perfect example. That is exactly why the article leans on the check instead of a list of "libraries with native code": the list changes with every dependency bump, and the APK is the only thing that tells the truth. Worth repeating the check after any plugin or AndroidX upgrade too, since a transitive library can bring in a new .so without a line of your own code changing. Good to hear the Flutter engine and AOT libraries came out clean on the current SDK.