Pocket Studio Academy
HomePart 66.5

Debug vs release builds

Free lesson10 min read·4 questions

Every app you have built so far was a debug build. There is a second kind, and it is a different app in almost every way that matters — smaller, faster, harder to inspect, and impossible to install until you have signed it. Here is exactly what changes.

You have only ever built half the story

Count the number of times you have pressed Run since Lesson 0.3. Dozens, probably a few hundred. Every single one of those produced the same kind of file: a .

There is another kind. It comes from the same source code, the same and the same phone, and it is a noticeably different app when it lands. It is smaller. It starts faster. Its internals are unreadable. And out of the box, no phone on earth will install it.

That second kind is the , and this lesson is the whole of the difference.

Think of it like this

Think about a play, on two different nights.

Rehearsal night. The house lights are up so people can see. Actors are holding scripts. Half the set is still on scaffolding, and there is bare plywood where the painted flats will go. Anybody can shout "stop" and the whole thing pauses while a problem gets sorted out. It is deliberately easy to interrupt.

Opening night. Lights down. No scripts. The scaffolding is gone and so is every prop that turned out not to be used. Nobody shouts stop. And on the door there is a programme with the theatre's name printed on it, because the audience is entitled to know who is responsible for what they are about to watch.

Same play. Same words. Two completely different evenings.

A debug build is the rehearsal: interruptible on purpose, nothing thrown away yet, and the work lights left on. A release build is opening night: stripped down, sealed up, and stamped with your name.

Where the two builds come from

Both are already in your project. You did not add them and you cannot remove them — every Android project starts with exactly two called debug and release.

Here is the real buildTypes block out of Dice Duel, unedited:

The four things a debug build does for you

Every one of these is a favour, and every one of them has to be undone before anyone else sees your app.

1. It signs itself. Android refuses to install an unsigned app — there is no such thing as an install without a signature. So Android generates a throwaway key the first time you build anything, keeps it in a file called the , and quietly signs every debug build with it. That is why pressing Run has never once asked you about keys.

2. It is . The finished app carries a flag saying "another program is allowed to attach to me and look inside". That is what makes a possible, and it is exactly what you would never want in an app on a stranger's phone.

3. It skips the shrinking. No , no , no deleting. Every class you compiled is in there, under its real name, including the thousands of Compose classes your one screen never touches. That is why the Dice Duel debug APK is about 8.7 MB for a game with one screen and one die.

4. It is fast to build. All of the above adds up: nothing to analyse, nothing to rewrite, no key to check. On a phone, that difference is minutes, not seconds.

Measured on a real phone

Focus Flow, built both ways in Pocket Studio on the same device:

BuildSizeBuild time
app-debug.apk19.58 MB270.8 s from cold
app-release.apk11.50 MB136.0 s

The release APK is 41% smaller — and here is the part worth pausing on: Focus Flow ships with isMinifyEnabled = false, so never ran. Not one class was deleted or renamed.

All of that saving came from the other favours being withdrawn: no debug metadata, no debuggable flag, and native libraries getting their debug symbols stripped instead of shipped whole. Shrinking is a separate saving on top, and we turn it on later in this lesson.

If you take one thing from this table, make it that "smaller" and "shrunk" are not the same word.

The four things a release build does instead

Take the favours away and you get the other build.

BehaviourDebugRelease
Signed withAn automatic throwaway keyYour own key, which you make
Can attach a debuggerYesNo
Unused code removedNoYes, when minifying is on
Names in the appRealShortened and meaningless
Extra checksNoneLint's fatal checks must pass
Installable straight awayYesNot until it is signed

The last row is the one that surprises people. Build a release APK today, with no key set up, and the build succeeds. It just hands you a file with a name that tells you exactly what is wrong with it:

text
1app/build/outputs/apk/debug/app-debug.apk
2app/build/outputs/apk/release/app-release-unsigned.apk

An is a complete, finished app. Every class, every picture, every string. Android will still not touch it. Lesson 6.6 is about fixing that.

9:41▲ ▮
app / build / outputs
📁 apk
📁 debug
🗎 app-debug.apk8.7 MB
📁 release
🗎 app-release-unsigned.apk
📁 logs
📁 mapping
The green one installs. The amber one is a finished app that no phone will accept, because nothing has signed it yet.

The outputs folder in the Pocket Studio file tree, after building both types.

Turning R8 on

is the shrinker. On a release build it walks your entire program starting from the entry points, works out which code can possibly be reached, and deletes the rest. Then it renames what survives: GameViewModel.rollDice() becomes something like a.b(), which is shorter to store and impossible to skim.

Two settings switch it on, and a third tells it where your rules live:

kts
1buildTypes {
2    release {
3        isMinifyEnabled = true
4        isShrinkResources = true
5        proguardFiles(
6            getDefaultProguardFile(
7                "proguard-android-optimize.txt"
8            ),
9            "proguard-rules.pro"
10        )
11    }
12}
  • runs R8 over your code.
  • does the same job for pictures, strings and layouts that nothing refers to. It only works when minifying is already on.
  • getDefaultProguardFile(...) pulls in the rules Google ships for you — the ones that stop R8 breaking Android itself.
  • proguard-rules.pro is your own file of , sitting in the app folder. Create it empty; you add to it only when something breaks.

These three apps ship with isMinifyEnabled = false, so this course has never measured a shrunk build of them. For a Compose app of this size, R8 usually removes well over half the file. What it definitely does is make a crash report unreadable, which is why R8 writes a every time it runs. Keep the one that matches each published version and you can translate a.b() back into GameViewModel.rollDice() later.

The classic release-only bug

R8 deletes what nothing calls. But some code is not called — it is found by name at run time, by a library reading a string. R8 cannot see that connection, so it deletes the class, and your app crashes in release and works perfectly in debug.

The fix is always a keep rule in proguard-rules.pro. Modern libraries ship their own rules, so this is rarer than it used to be — but if release crashes and debug does not, this is where you look first.

The task names

You already know assembleDebug from Lesson 2.5. Its siblings do the obvious thing:

text
1:app:assembleDebug     -> app-debug.apk
2:app:assembleRelease   -> app-release-unsigned.apk
3:app:bundleRelease     -> app-release.aab

The third one produces an rather than an APK. That is the format Google Play wants, and Lesson 6.7 explains why.

What this costs you: nothing

Worth saying plainly, because this is the lesson where the two worlds meet.

Every build in this course is a debug build. Part 0 through to the end of Part 6, all three apps, every checkpoint — debug, all of it. Debug APKs are what the Pocket Studio Free tier produces, and they are all you need to finish everything here and keep building your own apps afterwards.

The Free tier caps how many builds you get. That is the only limit you have run into, and it is why this course keeps telling you to read your change before you press Run. Nothing in Part 6 asks you to pay to carry on learning.

Try it in Pocket Studio

Look at both build types in your own project. Use Focus Flow, or any project you like.

  1. Open Pocket Studio, tap Projects, open Focus Flow.
  2. Tap Editor, open the file tree, open app/build.gradle.kts.
  3. Find the buildTypes block. Confirm for yourself that there is no debug block in it.
  4. Change isMinifyEnabled from false to true, then add the two lines under it and the proguardFiles(...) call exactly as shown above.
  5. Long-press the app folder in the file tree, choose New File, and name it proguard-rules.pro. Leave it completely empty. That is a valid rules file.
  6. Tap Build, then press Run. It builds and installs exactly as normal — because Run builds debug, and not one line of what you just typed applies to debug.
  7. Tap Editor and open app/build/outputs/apk/debug/. There is app-debug.apk, freshly rebuilt. Note its size.
  8. Look for a release folder beside it. There is not one, because you have never built that type. That empty space is what the next two lessons are for.
  9. If your Pocket Studio tier offers release builds, watch one happen: open the Terminal and run ./gradlew :app:assembleRelease. Read the output rather than skimming it, and look for :app:minifyReleaseWithR8 — that task does not exist in a debug build at all. The Free tier's output is debug APKs, so if Pocket Studio stops you here, that is the honest boundary, and nothing later in the course depends on crossing it.
  10. Leave isMinifyEnabled = true in place, or set it back to false. Either way, Run goes on building and installing the debug APK exactly as it always has.
Error Doctor5 common errors
Execution failed for task ':app:lintVitalRelease'. > Lint found fatal errors while assembling a release target.
MeansRelease builds run Lint's fatal checks; debug builds skip them entirely. So this is a build that fails only in release, on code that has been fine for weeks. Lint has found something it considers serious enough to block shipping — a missing translation, a wrong versionCode, a resource that will crash on an old phone.
FixScroll up. The lines above the failure name the exact file, line and check ID. Fix the reported issue rather than switching the check off — lintVitalRelease only fires on things that genuinely break released apps.
ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in build/outputs/mapping/release/missing_rules.txt.
MeansA library in your project mentions a class that is not actually present. In debug this never mattered, because nothing looked. R8 walks the whole program, notices the gap, and stops rather than shipping something that might crash.
FixOpen the file it names — build/outputs/mapping/release/missing_rules.txt. R8 has already written the exact rules you need. Copy them into app/proguard-rules.pro and build again.
adb: failed to install app-release-unsigned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from base.apk: Attempt to get length of null array]
MeansExactly what the filename warned you about. The APK is complete and valid, but nothing signed it, so there is no certificate for Android to read. It refuses before it even unpacks the thing.
FixNothing is wrong with your code. An unsigned release APK is not installable by design. Either install the debug build, or set up a — which is Lesson 6.6.
Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.nativeworks.focusflow signatures do not match previously installed version; ignoring!]
MeansYou already have the debug build of this app installed, and you are now trying to install a release build over the top. Same applicationId, different key. Android treats those as two different apps by two different people, and will never let one replace the other.
FixUninstall the existing copy from your home screen first, then install the new one. This is not a bug — it is precisely the protection that signing exists to provide, working correctly.
Execution failed for task ':app:minifyReleaseWithR8'. > A failure occurred while executing com.android.build.gradle.internal.tasks.R8Task$R8Runnable > java.lang.OutOfMemoryError: Java heap space
MeansR8 has to hold your entire program in memory at once to work out what is reachable. On a phone, with the default 2 GB limit, a Compose app can be just big enough to run it out of room.
FixOpen gradle.properties in the project root and raise the number in org.gradle.jvmargs=-Xmx2048m to -Xmx3072m. Close your other apps first, or the phone will simply refuse to hand that memory over.
Recap
  • Every project has two from the start. debug needs no block in your build file; release is the one you configure.
  • A signs itself with the shared , stays , keeps every class, and builds fast. All four are favours for testing.
  • A does none of that. assembleRelease with no key produces app-release-unsigned.apk — a finished app no phone will install.
  • turns on, which deletes unreachable code and renames what survives. Keep the it writes.
  • Release builds also run 's fatal checks, so a build can fail in release that has been green in debug for weeks.
  • The whole of this course runs on debug builds, which the Pocket Studio Free tier produces. Nothing here asks you to pay to keep learning.
  • Next: the missing piece — what a signature actually proves, how to make a key of your own, and the one file you must never lose.