What an APK really contains
An APK is a ZIP file with a fixed set of contents. Open a real one, entry by entry — the dex files that hold your code, the compiled resource table, the binary manifest, the native libraries, and the signature that proves it came from you.
You can open it. Right now.
An is not a mysterious binary format. It is a archive with rules about what must be inside it.
That means you can open one, today, on your phone, with an ordinary file manager. And you should, because seeing the pieces laid out turns "the build produced something" into "I know exactly what came out."
Everything in this lesson is measured from a real file: app-debug.apk, built from the Dice Duel project in this course. 8.7 MB, 107 entries.
Think about a flat-pack bookcase arriving in a box.
Inside there are the panels — the heavy part, the actual furniture. There is a parts list so you can find dowel B without opening every bag. There is a label on the outside saying what the box is, so the warehouse knows what it has without opening it. And there is a tamper-proof seal, so you know nobody swapped the contents on the way.
An APK has all four. The panels are your code. The parts list is the compiled resource table. The label is the manifest. The seal is the signature — and Android refuses to install a box whose seal is missing.
What is actually in there
Real entries, real sizes, from the Dice Duel debug build:
1classes.dex 17,073,596
2classes5.dex 4,680,756
3resources.arsc 416,312
4classes4.dex 75,836
5classes2.dex 33,736
6classes3.dex 9,532
7AndroidManifest.xml 5,280
8res/ ............... 41 entries
9lib/ ............... 4 entries
10META-INF/ .......... 47 entries
11kotlin/ ............ 7 entriesNotice something odd: classes.dex alone is 17 MB, but the whole APK is 8.7 MB. Those are uncompressed sizes. A ZIP squeezes its contents, and code compresses extremely well.
classes.dex — your code
This is where every line of Kotlin you wrote ends up, along with every library you used.
The journey takes two steps:
- The Kotlin compiler turns your source into JVM — the same format a Java program uses.
- converts that bytecode into , the compact format Android's runtime actually executes.
Why the second step? Because , the Android Runtime, does not read JVM bytecode. Dex was designed for phones: smaller files, faster to load, and laid out so many classes can share one lookup table.
Why there are five of them
A single dex file can only reference 65,536 methods. That sounds enormous until you count Compose, AndroidX and the Kotlin standard library together — a modern app blows past it easily.
The build's answer is to split across several files: classes.dex, classes2.dex, and so on. Android loads them all. You do nothing; it happens automatically.
Why a debug APK is so big
8.7 MB for a dice game seems a lot, and it is. A deliberately skips the shrinking step.
On a with isMinifyEnabled = true, deletes every class and method nothing actually uses, and shortens the names of what remains. The same app typically comes out a fraction of the size. Lesson 6.5 does that properly and shows the numbers.
resources.arsc — the parts list
416 KB, and it is the reason Lesson 2.4's cost nothing at run time.
is a compiled table: every string, colour and dimension in your app, each with its number from the , and one row per configuration. Your values/ theme and your values-night/ theme sit side by side in it as two rows of the same entry.
When Android needs a colour, it does not search folders. It looks up a number in a table it has already memory-mapped, checks the current configuration, and returns the matching row.
AndroidManifest.xml — but not as you wrote it
There is an AndroidManifest.xml in the APK, and it is 5 KB — noticeably bigger than the file you wrote.
Two reasons. First, it is the merged manifest: yours plus every library's, exactly as Lesson 2.3 described. Second, it is not text at all. compiled it into binary XML, so Android can read it without parsing angle brackets at install time.
If you open it in a text editor you will see mostly gibberish with a few readable names. That is correct — you are looking at a compiled file.
res/ — the files that stayed files
41 entries. Anything that could not be reduced to a row in a table: the icon XML, the vector drawables, the layouts that Compose libraries still ship for notifications.
Note that res/values/strings.xml is not in the list. Values files do not survive as files — their contents were folded into resources.arsc.
lib/ — code for the processor itself
1lib/arm64-v8a/libandroidx.graphics.path.so
2lib/armeabi-v7a/libandroidx.graphics.path.so
3lib/x86/libandroidx.graphics.path.so
4lib/x86_64/libandroidx.graphics.path.soSome code is not run by ART at all. A is compiled straight to processor instructions, so it has to be built once for each — each family of chip. The same library appears four times, once per instruction set, and the phone loads only the one that matches it.
Dice Duel never asked for this. It arrived with Compose, which uses a small native helper for drawing paths. It is a good, small illustration of what a actually brings with it.
META-INF/ — the seal, and some receipts
47 entries, of two kinds.
Receipts. Files like androidx.compose.material3_material3.version — one per library, recording exactly which version shipped. Diagnostic only.
The signature. This is the interesting one, and it is not where you would expect.
Old APKs were signed like a Java JAR: files called MANIFEST.MF and CERT.RSA inside META-INF. Our APK has neither, because modern builds use a newer scheme. The signature is written into an APK Signing Block — a chunk placed between the file data and the ZIP index, outside the list of entries entirely.
In our real 8,735,803-byte APK it begins at byte 8,725,970: right near the end, just before the index.
The advantage is that the newer scheme covers the whole file, so nothing can be altered without breaking it — not even a filename. Android checks it at install time and refuses anything that fails.
Your debug APK is signed automatically with a throwaway debug key, which is why it installs on your phone with no ceremony. Publishing needs your own key, and Lesson 6.6 covers properly.
One last quiet step: . It nudges entries so they start on tidy boundaries, which lets Android read them straight from storage instead of copying them into memory first. The build runs it for you.
app-debug.apk, opened as a ZIP. Real entries and real uncompressed sizes from the Dice Duel debug build.
APK or AAB?
One last distinction, so the words are not confusing later.
An APK is a finished, installable app for one device. It is what you build, what you install on your own phone, and what you would send a friend.
An — Android App Bundle — is what Google Play wants instead. It is not installable. It is a package of everything needed to make APKs, and Play generates a tailored one per device: only that phone's , only its screen density, only its language. Smaller downloads for users, no work for you.
Same build, one extra task. Lesson 6.7 covers it when it matters.
Open a real APK and look inside.
- Open Pocket Studio, tap Projects, open your Part 0 app.
- Tap Build and press Run, so a fresh APK definitely exists.
- Tap Editor, open the file tree, and navigate to
app/build/outputs/apk/debug/. There it is:app-debug.apk. - Note its size. Anything from about 4 MB to 10 MB is normal for a debug Compose app.
- Go back to Build and open the Terminal. Type these two lines:
cd app/build/outputs/apk/debugthenunzip -l app-debug.apk - Read the listing against this lesson. Find
classes.dex,resources.arsc,AndroidManifest.xmland thelib/folder. - If your terminal has no
unzip, do it the other way: long-pressapp-debug.apkin the file tree, copy it somewhere you can reach, rename the copy toapp-debug.zip, and open it with your phone's Files app. It will browse it like any other archive. - Try opening
AndroidManifest.xmlfrom inside the archive in a text editor. The gibberish is the point — it is compiled binary XML, not the file you wrote.
minSdk 21 and above the build normally splits the code across several dex files by itself. If you see this, add multiDexEnabled = true to defaultConfig. The better long-term fix is fewer dependencies.android { } add a packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1,LICENSE.md}" } } block naming the paths from the error.build folder and the Gradle cache are also taking space on the same phone.- An is a with required contents. You can open one on your phone right now.
classes.dex(and its numbered siblings) hold your code, converted from to by because one dex file caps out at 65,536 methods.- is the compiled resource table, with a row per configuration — that is why dark mode is free at run time.
- The
AndroidManifest.xmlinside is the merged one, compiled to binary XML. lib/holds , one copy per .- The signature lives in an APK Signing Block near the end of the file, not as an entry. Android refuses to install anything unsigned.
- Next: Part 3 — , and the first screen you design yourself.