¿Por qué firmar?
Android requiere que toda app esté firmada digitalmente antes de instalarse. La firma identifica al autor de la app y garantiza que las actualizaciones vienen de la misma persona — si alguien intercepta tu APK y lo modifica, la firma no va a coincidir y Android rechazará la instalación.
En desarrollo, Android Studio firma automáticamente con un debug keystore genérico. Para publicar en Play Store necesitás tu propio release keystore.
Crear el keystore
# Crear un keystore con keytool (incluido en el JDK)
keytool -genkeypair \
-v \
-keystore mi-app.keystore \
-alias mi-app-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
# El comando pedirá:
# - Contraseña del keystore
# - Contraseña de la clave
# - Nombre, organización, ciudad, país (para el certificado)
# También podés crearlo desde Android Studio:
# Build → Generate Signed Bundle / APK → Create new...
El keystore es irreemplazableSi perdés el keystore, perdés la capacidad de publicar actualizaciones de tu app en Play Store. Los usuarios tendrían que desinstalar e instalar la nueva versión. Guardalo en al menos dos lugares seguros (gestor de contraseñas, backup cifrado). Google recomienda usar Play App Signing — ellos guardan tu clave upload y mantienen una clave de firma separada.
Signing config en Gradle
// build.gradle (app)
android {
signingConfigs {
create("release") {
storeFile = file("../keystore/mi-app.keystore")
storePassword = "contraseña_del_keystore"
keyAlias = "mi-app-key"
keyPassword = "contraseña_de_la_clave"
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
minifyEnabled = true
shrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
Credenciales seguras — no en el código
Nunca pongas las contraseñas del keystore directamente en el build.gradle — ese archivo va al repositorio. Hay dos formas correctas:
// Opción 1: variables de entorno (ideal para CI/CD)
signingConfigs {
create("release") {
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "../keystore/mi-app.keystore")
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
keyAlias = System.getenv("KEY_ALIAS") ?: "mi-app-key"
keyPassword = System.getenv("KEY_PASSWORD") ?: ""
}
}
// Opción 2: archivo local.properties (en .gitignore)
// En local.properties (NO commiteado):
// KEYSTORE_PASSWORD=mi_contraseña
// KEY_PASSWORD=mi_contraseña_clave
// En build.gradle:
import java.util.Properties
val localProps = Properties()
val localPropsFile = rootProject.file("local.properties")
if (localPropsFile.exists()) localProps.load(localPropsFile.inputStream())
signingConfigs {
create("release") {
storeFile = file("../keystore/mi-app.keystore")
storePassword = localProps.getProperty("KEYSTORE_PASSWORD") ?: ""
keyAlias = "mi-app-key"
keyPassword = localProps.getProperty("KEY_PASSWORD") ?: ""
}
}
Agregá el keystore al .gitignoreEl archivo .keystore tampoco debe ir al repositorio. Guardalo en un lugar seguro externo al repo. En el .gitignore: *.keystore, *.jks, local.properties.
Build types
Los build types permiten tener configuraciones diferentes para distintos entornos:
android {
buildTypes {
debug {
applicationIdSuffix = ".debug" // instala junto a release
versionNameSuffix = "-debug"
isDebuggable = true
// No necesita signing config — usa el debug keystore automáticamente
}
// Build type personalizado para QA/staging
create("staging") {
initWith(getByName("debug")) // hereda la config de debug
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
isDebuggable = true
signingConfig = signingConfigs.getByName("debug")
// Se puede conectar a un backend de staging
}
release {
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
Product flavors
Los flavors permiten generar múltiples variantes de la misma app — por ejemplo, versión free/paid, o versión para distintos clientes:
android {
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationIdSuffix = ".free"
versionNameSuffix = "-free"
// Puede tener su propio set de resources en src/free/res/
}
create("paid") {
dimension = "version"
applicationIdSuffix = ".paid"
versionNameSuffix = "-paid"
}
}
}
// En código Kotlin podés usar BuildConfig para comportamiento diferente:
if (BuildConfig.FLAVOR == "paid") {
habilitarFuncionesPremium()
}
Build variants
La combinación de build types y flavors genera los build variants. Con los ejemplos anteriores tenés:
# Variantes generadas automáticamente:
freeDebug
freeStagging
freeRelease
paidDebug
paidStaging
paidRelease
# Cada variante tiene su propio APK/AAB
# Podés verlos en Android Studio → Build Variants (panel inferior izquierdo)
# Compilar una variante específica:
./gradlew assembleFreeRelease
./gradlew bundlePaidRelease # genera AAB