When building Android apps you often need sensitive values like API keys, client IDs, identity providers. Committing these into your version control or having public visibility risks security concerns and is a recipe for disaster. We’ll walk through a simple, maintainable pattern which could help you.
Since those values are required in runtime while building/testing applications they need to be made available locally for build.
One solution is , for this purpose we can rely on local.properties to have that sensitive information for below reasons :
- By default ignored by git it becomes already convenient. (If not, add it to .gitignore)
- Available for both your Gradle scripts and IDE since they are in project root.
- Requires no additional plugins.
- Extends into CI/CD via GitHub Secrets so your tests run uninterrupted.
1. Keeping track of secrets and managing them
The Onboarding Pain Point
Imagine a new teammate clones your repo and builds the project.
gradlew assembleDebug
And only to run into build failures.
They check build files, but can’t find where API_KEY is supposed to come from. This hidden dependency slows everyone down, leads to increases the risk that someone might accidentally commit their real keys just to make things work.
Defining a Clear Contract
To avoid these pitfalls, you need a single, versioned contract that tells everyone exactly which secrets your build requires. That contract should:
- Be discoverable for everyone who uses the repository.
- Never contain real values, so it’s safe to share publicly.
- Human readable, so no one has to guess or reverse engineer to find keys.
Introducing keys.properties.md
At the project root, create a file called keys.properties.md, which will be in version control which documents only placeholders but no values.
# keys.properties.md
> This file lists all the keys you need to define in your `local.properties` file.
> To avoid committing real `local.properties` into version control we use this as template.
# API key from TMDB
API_KEY=
With these placeholders, others immediately know which keys to supply to the build.
Now once they are aware these, after they get the required keys, they can insert those to their local file local.properties which is not tracked in version control. And it could look like :
sdk.dir=C\:\\Users\\name\\AppData\\Local\\Android\\Sdk
# API key
API_KEY=whateverkeyyouhave
Now we will functions which helps us inject these keys via Gradle.
2. Injecting Secrets with Gradle
At this point, you have keys in your properties file and now we need to access it and allow Gradle to provide it in places required in project.
This function in your build.gradle.kts will allow us to read those keys from properties file.
import java.util.Properties
fun getLocalProperties(): Properties {
return Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) {
file.inputStream().use { load(it) }
}
}
}
Below extension function will access those keys from our properties function and emits into BuildConfig file which is a build generated file.
fun ApplicationDefaultConfig.setupBuildConfigFields(
properties: Properties,
) {
fun secret(key: String): String = properties.getProperty(key, "")
buildConfigField(type = "String", name = "API_KEY", value = "\"${secret("API_KEY")}\"")
}
Call this in your android defaultConfig block :
android {
defaultConfig {
val properties = getLocalProperties()
setupBuildConfigFields(properties = properties)
}
}
After sync you can verify those values will be available in BuildConfig file. You can access it everywhere in project, example :
object TmdbApiKey {
const val API_KEY = BuildConfig.API_KEY
}
3. Validation for secrets and fail builds
You can take advantage of current Gradle setup and throw errors to Devs when keys are missing or not provided.
In the same function, you can validate keys and warn with failing builds.
fun ApplicationDefaultConfig.setupBuildConfigFields(
properties: Properties,
) {
fun secret(key: String): String = properties.getProperty(key, "")
if (secret("API_KEY").isEmpty()) {
error("API_KEY not set in local.properties")
}
buildConfigField(type = "String", name = "API_KEY", value = "\"${secret("API_KEY")}\"")
}
If the key is missing build will fail with Exception as below :
API_KEY not set in local.properties
* Exception is:
java.lang.IllegalStateException: API_KEY not set in local.properties

4. Wiring Up CI/CD with Secrets
Locally you use local.properties, but your CI runner starts with a clean workspace. Here’s how to ensure builds pass as we will try to provide the API_KEY as an example.
In your repository,
- Open settings
- Secrets and variables
- Actions
- Add the secret key attribute and value.
Now we need to provide that same attribute name to receive it’s value in our workflow environment.
jobs:
build:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.API_KEY }}
Failing to do these steps will result your actions to fail since it can’t find the key/value since properties file in only available locally in your project setup.
- No need to generate a fake properties file on the server.
- Secrets remain encrypted at rest in GitHub, never revealed in your code.
Conclusion :
With this approach we solve below things :
- Security risk: Anyone cloning your public repository could extract keys.
- Rotation pains: If you need to rotate a key, hunting through code is error-prone.
- CI/CD concerns: Build servers shouldn’t embed real secrets in artifacts.
Reference :
Project with working examples
Workflow file with mentioned setup
Author

Raj
Hi ! I’m a Software Engineer at MedKitDoc & Technical writer.
I lead organization Developers Breach focusing on contributing to Open Source and Student Projects.
