DEV Community

Cover image for Building and Signing Android Apps Locally with Gradle (React Native & Expo)
Omotola Odumosu
Omotola Odumosu

Posted on

Building and Signing Android Apps Locally with Gradle (React Native & Expo)

A practical, step-by-step walkthrough of generating debug and release builds, configuring keystores, fixing common Gradle errors, and preparing your app for Google Play.

If you’ve been using EAS Build, you already know how convenient it is. EAS automatically creates your keystore and signs your app for you, which makes building Android apps feel almost effortless.

However, you also discover the downside pretty quickly: long queues. Sometimes your build sits for hours before it even starts. That’s usually the moment you realize that building locally is not optional; it’s a skill you need.

When I attempted my first Android release build locally, I ran into a lot of confusion, debugging, research, and errors like:

  • debug vs release signing
  • keystore vs signingReport
  • Why my APK worked but wasn’t Play Store–ready
  • random Gradle errors that made no sense at first

And many more that I can’t fully cover here.

Even if the exact issue you’re facing isn’t listed above, don’t worry, just follow along, and we’ll get to the destination together.

For this article, I’ll be using React Native with Expo on Windows, but most of the concepts apply generally across platforms.

Prerequisites
Before anything else, make sure you have:

  • JDK 17 installed
  • Android Studio
    • Android SDK
    • Android SDK Build Tools
  • Node.js + npm
  • A React Native or Expo project

Step 1: Generate the Android Folder (Expo users)

If you’re using Expo (managed workflow), you first need the native Android files. From your project root:

npx expo prebuild --platform android
Enter fullscreen mode Exit fullscreen mode

This generates an Android folder in your project.

Step 2: Move into the Android Directory

cd android
Enter fullscreen mode Exit fullscreen mode

All Gradle commands are run inside the Android folder, not the project root.

Step 3: Clean the Project (Important)

Before building anything, always clean once:

./gradlew clean
Enter fullscreen mode Exit fullscreen mode

This command clears old build artifacts and ensures Gradle starts from a clean state. You usually run this once per project, unless

  • you hit strange build errors,
  • you change signing configs, or
  • You change native dependencies

Common Windows Issue
If you come across this error, "Filename longer than 260 characters." It means the path to your project is too long.

Fix:
Move your project closer to the drive root, e.g.

C:\my-project
Enter fullscreen mode Exit fullscreen mode

Step 4: Understanding Build Types

Gradle can generate different build types, such as:

Development build

A development build can be generated using the command below

./gradlew assembleDebug
Enter fullscreen mode Exit fullscreen mode

Development builds are:

  • Signed with debug keystore
  • Used for local testing while coding, just like Expo Go
  • Not for Play Store

Release APK

A release build can be generated with the code below

./gradlew assembleRelease
Enter fullscreen mode Exit fullscreen mode

This produces a release APK that can be installed on your device and used. It is fully functional, but not necessarily Play Store ready.

Play Store Bundle (Recommended)

Play Store doesn't accept APK files for app uploads anymore, and as such, an AAB file is required as per Play Store's requirements. The command below generates a .aab file

./gradlew bundleRelease
Enter fullscreen mode Exit fullscreen mode

Step 5: Where Your Builds Are Located

After a successful build, your apps are located in the directory below:

APKs

android/app/build/outputs/apk/debug/app-debug.apk
android/app/build/outputs/apk/release/app-release.apk
Enter fullscreen mode Exit fullscreen mode

If you initiated a debug APK build, you'll find your app in the first path above, but if you initiated a release build, you'll find your app in the second path above.

Play Store Bundle

android/app/build/outputs/bundle/release/app-release.aab
Enter fullscreen mode Exit fullscreen mode

For Google Play Store builds (.aab), you'll find your app in the directory above

Step 6: Understanding signingReport

To see how your app is signed, run this command:

./gradlew signingReport
Enter fullscreen mode Exit fullscreen mode

This shows:

  • Variant (debug / release)
  • Config (which keystore was used)
  • Store file
  • Alias
  • SHA1 / SHA256 fingerprints

Important Discovery 💡

If you ran a release build in step 4 above, and after running the command in step 6, you see:

variant: release
config: debug
Enter fullscreen mode Exit fullscreen mode

Config tells you which keystore was actually used, not the build type you intended to use. Seeing the above means that your release APK is still signed with the debug keystore. The implication of this is that your app will work normally and will be fully functional; you can share it with friends or anyone, and it will still work, but you can't upload it to the Google Play Store. It will get rejected because the app was signed with a debug keystore. If your intention wasn’t to upload to the Play Store but maybe to just have the app and test that it works fine, or share it with friends, then you have no issues to bother about. However, if you plan to upload to the store, you need to generate a release keystore so your app can be signed with that release keystore.

Step 7: Generating a Release Keystore (Windows)

Open PowerShell as administrator, and navigate to the directory below:

C:\Program Files\Java\jdkX.X.X\bin
Enter fullscreen mode Exit fullscreen mode

Then run this command:

.\keytool.exe -genkeypair -v `
  -storetype PKCS12 `
  -keystore my-upload-key.keystore `
  -alias my-key-alias `
  -keyalg RSA `
  -keysize 2048 `
  -validity 10000
Enter fullscreen mode Exit fullscreen mode

You’ll be asked to set the following:

Name, organization, country, Keystore password, Key password, etc.

Important
Save your passwords and aliases. You can’t recover them later. And also, while typing the passwords, your terminal won't show the letters you're typing at all; that's a security measure put in place to secure your password, so be very careful when typing

Step 8: Back Up Your Keystore (Very Important)

Create a backup folder with this code :

mkdir "$env:USERPROFILE\Documents\android-keys"
Enter fullscreen mode Exit fullscreen mode

Then use this code to duplicate the keystore to the folder created

copy my-upload-key.keystore "$env:USERPROFILE\Documents\android-keys"
copy my-upload-key.keystore "my-upload-key.BACKUP.keystore"
Enter fullscreen mode Exit fullscreen mode

This code also renames the duplicated version to this name "my-upload-key.BACKUP.keystore"

To verify if the duplication was successfully performed, run this code

dir "$env:USERPROFILE\Documents\android-keys"
Enter fullscreen mode Exit fullscreen mode

This code will show the directory to the duplicated keystore

Important
If you lose this keystore, you permanently lose the ability to update your app on the Play Store, so you can always copy your keystore to an external hard-drive or flash.

Step 9: Verify Keystore and Alias

To verify the existence of your keystore and key-alias, run this code

.\keytool.exe -list -keystore my-upload-key.keystore
Enter fullscreen mode Exit fullscreen mode

This code lists the aliases available. It will ask for your keystore password. if password is correct, you should see something like this

"my-key-alias, PrivateKeyEntry"

This confirms your alias exists. However, this won't show the full details of the key-alias; it just confirms whether your alias actually exists. its basically a confirmation check.

To View full details of your alias

To see the full details of your key-alias and other credentials, run this

.\keytool.exe -list -v `
  -keystore my-upload-key.keystore `
  -alias my-key-alias
Enter fullscreen mode Exit fullscreen mode

This shows your:

  • Alias name,
  • Creation date,
  • Certificate fingerprints (SHA1, SHA256),
  • Validity period.

It will never show passwords (that’s correct).

Step 10: Configure Gradle Properties

Edit "android/gradle.properties" by adding this at the bottom of the file :

MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystore
MYAPP_UPLOAD_KEY_ALIAS=my-key-alias
MYAPP_UPLOAD_STORE_PASSWORD=*****
MYAPP_UPLOAD_KEY_PASSWORD=*****
Enter fullscreen mode Exit fullscreen mode

Important
If you followed step 7 without changing anything there, then all you need to add above is your password that you set when creating your release keystore in step 7. But if you changed the alias name template or any other field there, then you need to put whatever name you used.

Step 11: Configure Release Signing in build.gradle

Navigate into "android/app/build.gradle" file and add this part :

android {
    ...

    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
                storeFile file(MYAPP_UPLOAD_STORE_FILE)
                storePassword MYAPP_UPLOAD_STORE_PASSWORD
                keyAlias MYAPP_UPLOAD_KEY_ALIAS
                keyPassword MYAPP_UPLOAD_KEY_PASSWORD
            }
        }
    }

    buildTypes {
        release {
// Important: ensure ONLY ONE signingConfig exists here.
// If debug is present, remove it and leave only release.

            signingConfig signingConfigs.release
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 12: Build Your App Again (This Time Properly Signed)

Since you have correctly set up the release configuration, building your app again will now use the release configuration to sign your app, making it acceptable by playstore

./gradlew clean
./gradlew assembleRelease
Enter fullscreen mode Exit fullscreen mode

After your build has completed, let's check which signing configuration was used just to be sure everything works as it should. Run this code

./gradlew signingReport
Enter fullscreen mode Exit fullscreen mode

You should see

variant: release
config: release
Enter fullscreen mode Exit fullscreen mode

This confirms the correct keystore is being used. SigningReport always reflects the current Gradle configurations used for our most recent build, not past builds.

Step 13: Generate Upload Certificate for Play Store (Final Step)

Google Play requires your upload certificate (done once per app). Run:

.\keytool.exe -export -rfc `
  -keystore my-upload-key.keystore `
  -alias my-key-alias `
  -file upload_certificate.pem
Enter fullscreen mode Exit fullscreen mode

To verify the upload certificate was created successfully, Run

notepad upload_certificate.pem
Enter fullscreen mode Exit fullscreen mode

You should see something like this

-----BEGIN CERTIFICATE-----
MIIF...
-----END CERTIFICATE-----
Enter fullscreen mode Exit fullscreen mode

Upload this file to Google Play Console → App Signing.

Common Error I Faced (And the Fix)

Error

Execution failed for task ':app:mergeReleaseNativeLibs'
2 files found with path 'lib/arm64-v8a/libworklets.so'
Enter fullscreen mode Exit fullscreen mode

Cause

Both:

  • react-native-reanimated
  • react-native-worklets were installed.

In modern React Native, react-native-worklets should NOT be installed separately

Fix

npm uninstall react-native-worklets
cd android
./gradlew clean
./gradlew assembleRelease
Enter fullscreen mode Exit fullscreen mode

In Conclusion
This process felt confusing at first, but if you made it here, I am sure you now have an app ready for Google Play Store upload. Take note that you don’t need to manually delete old APKs before building a new one; Gradle overwrites them automatically. SigningReport reflects current Gradle config, not past builds. Google Play prefers .aab files, not APKs. Back up your keystore like your career depends on it (because it does)


Did this post help simplify things for you?

If yes, drop a ❤️ or 🦄 reaction and follow me here on dev.to. I share more practical, plain-English breakdowns like this.

You can also connect with me on social media. I’d love to learn, share, and grow together with you!

LinkedIn: LinkedIn
Twitter: Twitter
Instagram: Instagram
Graphics Credit: Torcheux Frédéric

Top comments (0)