Homelab the Right Way

Home › Posts

The two decisions you can't undo before your first Google Play upload

Your application ID and your signing setup are the two Android decisions that are expensive to change after the first upload. Here is how to get both right, plus the versionCode rule that catches people later.

· 4 min read · androidgoogle playkotlingradleapp signing

The two decisions you can't undo before your first Google Play upload

Most of an Android project can be changed later. A screen can be redesigned, a library swapped, a name on the store edited. Two things are different. The application ID and the signing setup are decided before your first upload, and changing either one afterwards ranges from painful to impossible.

Both take minutes to get right. Here is what to do, with the code from Ship Your First Android App.

The trap: com.example goes live

Every tutorial starts with com.example.something. It works fine on your phone, so it is easy to forget. Then the first bundle goes up to Play Console and the placeholder becomes the app's permanent identity.

Android's documentation is blunt about this. Once you publish, you should never change the applicationId in app/build.gradle.kts, because if you change it, Google Play treats the upload as a completely different app. Your installs, ratings and reviews stay with the old ID.

The fix: pick an ID you can live with forever

The naming rules from developer.android.com:

  • At least two segments, separated by dots.
  • Each segment starts with a letter.
  • Only letters, digits and underscores: [a-zA-Z0-9_].

The convention is your domain, reversed, plus the app: com.yourname.torchlight. If you have no domain, a reversed form of a name you control works. What matters is that it is unique and yours. Replace com.example before the first upload, while it is still free to change.

One thing that confuses people: namespace and applicationId start out identical, but they are different things. The namespace is only the Kotlin package for the generated R and BuildConfig classes and can be refactored any time. The application ID is the public identity. Changing one does not change the other.

The second decision: how your app is signed

Every Android app is signed, and devices only install an update signed by the same key as the installed version. Google Play splits this into two keys:

KeyWho holds itWhat it signsIf you lose it
Upload keyYou, in a keystore file (.jks)The bundle you upload to Play ConsoleGoogle can reset it; you request a reset
App signing keyGoogle PlayThe APKs delivered to users' devicesNothing to lose; Google keeps it

Play App Signing is required for new apps, and new apps are automatically enrolled with Google-generated keys. Your job is only the upload key. Play Console asks for an RSA key of 2048 bits or more in a Java keystore, and Android's signing guide recommends a validity of at least 25 years:

keytool -genkeypair -v \
  -keystore upload-keystore.jks \
  -alias upload \
  -keyalg RSA -keysize 2048 \
  -validity 10000

-validity is in days. 10,000 days is a little over 27 years.

Where the key must not live

The common mistake is keeping the keystore in the project folder and committing it to Git, or keeping the only copy on one laptop. Store the .jks file outside the repository, back it up in at least two places (for example an encrypted cloud drive and a USB stick), and keep the passwords in a password manager. Google can reset a lost upload key, but a reset takes a support request and waiting, and a leaked key lets someone else upload as you until it is reset.

Wire signing into Gradle without leaking passwords

Never write passwords into build.gradle.kts. Put them in a keystore.properties file in the project root:

# keystore.properties (add this file to .gitignore!)
storeFile=/home/you/keys/upload-keystore.jks
storePassword=your-keystore-password
keyAlias=upload
keyPassword=your-key-password

And add these lines to .gitignore right away:

keystore.properties
*.jks
*.keystore

Then load it in app/build.gradle.kts. This version also reads environment variables, so the same file works on a build server later, and someone who clones the project without the keys still gets a working unsigned build instead of an error:

import java.io.FileInputStream
import java.util.Properties

val keystoreProps = Properties().apply {
    val f = rootProject.file("keystore.properties")
    if (f.exists()) FileInputStream(f).use { load(it) }
}
fun signingValue(key: String, env: String): String? =
    System.getenv(env) ?: keystoreProps.getProperty(key)

android {
    signingConfigs {
        create("release") {
            val path = signingValue("storeFile", "RELEASE_STORE_FILE")
            if (path != null) {
                storeFile = file(path)
                storePassword =
                    signingValue("storePassword", "RELEASE_STORE_PASSWORD")
                keyAlias = signingValue("keyAlias", "RELEASE_KEY_ALIAS")
                keyPassword =
                    signingValue("keyPassword", "RELEASE_KEY_PASSWORD")
            }
        }
    }
    buildTypes {
        release {
            optimization { enable = true }
            // Sign only when keys are available; otherwise build unsigned.
            if (signingValue("storeFile", "RELEASE_STORE_FILE") != null) {
                signingConfig = signingConfigs.getByName("release")
            }
        }
    }
}

The book's example comes from a real repository: the author's Flashlight app (chartmann1590/Flashlight) ships a keystore.properties.example with empty values, so anyone who clones the project knows exactly what to provide without anything being exposed.

A bonus rule: versionCode only goes up

Every upload needs a versionCode greater than the one before, and a value can never be reused. That includes builds uploaded to internal tests and builds you later abandon. The author's Dosely app is the book's example: version 1.1.1 had to go out as versionCode 4 because versionCode 3 had already been used by an earlier upload.

The greatest value Google Play allows is 2100000000. Don't jump to a huge number "to be safe" or use a full date-and-time stamp; it can get close to the ceiling quickly. Small, steady increments last forever:

defaultConfig {
    versionCode = 1        // 1, 2, 3 ... one more for every upload
    versionName = "1.0.0"  // major.minor.patch, for humans
}

Checklist before the first upload

  • applicationId is yours, not com.example, and follows the naming rules
  • Upload key created: RSA 2048 or more, valid 25 years or more
  • Keystore stored outside the repo and backed up in two places
  • Passwords in a password manager; keystore.properties and *.jks in .gitignore
  • ./gradlew bundleRelease produces app-release.aab
  • The release variant installed and tested on a real phone (./gradlew installRelease)
  • versionCode increased since the last upload

More articles