我开发的flutter 项目已经上架了App store。近期要上传Google Play,我也是第1次上架,边学边记录。希望能帮到有需要的人。
前提
- flutter项目已经开发好了,应用图标,名称,启动图这些都好了。
- Google Play 开发者账号已经申请好了。
本文只介绍,对我来说必须做的事情。其他细节建议自行查官方文档。
flutter官方文档
从哪开始?问AI或者官方文档,我这里选择看官方文档:
为 app 签名
为了将你的应用发布到 Play 商店,你必须给你的应用进行数字签名。
Android 应用需要两个签名:上传签名 和 应用签名。
- 开发者上传到 Play Store 的 .aab 或 .apk 需要有
上传签名。 - 终端用户下载的 .apk 文件需要有
应用签名。
请参考 Play Store 的官方文档 来创建你的应用签名。
应用签名很常见,做过android相关开发的,不管是原生开发,还是hbuilder之类的,RN 都应该接触过。我这边以前写过类似的文档
参考以下步骤对你的应用进行签名。
创建一个用于上传的密钥库
如果你已经有一个密钥库了,可以直接跳到下一步,如果还没有,需要参考下面的方式创建一个:
在 macOS 或者 Linux 系统上,执行下面的代码:
bash
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA \
-storetype JKS -keysize 2048 -validity 10000 -alias upload
keytool这个命令是jdk中的,我这里有的是JDK11,建议用JDK 9以上的版本。
这里根据提示输入密码,公司信息即可。请妥善保管这个文件。
从 app 中引用密钥库
创建一个名为 project/android/key.properties 的文件,它包含了密钥库位置的定义。在替换内容时请去除 < > 括号:
properties
storePassword=<password-from-previous-step>
keyPassword=<password-from-previous-step>
keyAlias=upload
storeFile=<keystore-file-location>
在 Gradle 中配置签名
这步,建议看 https://docs.flutter.cn/deployment/android#configure-signing-in-gradle
因为我的环境有点特殊,就不在这里写出来了,怕误导读者。
继续探索
以上内容本质上就是普通的android打包流程,那么上传Google Play 相关的呢?
Flutter文档中并没有详细的介绍,只是引用了一个链接
https://developer.android.com/distribute?hl=zh-cn
我看完以后还是不清楚怎么做,于是只能换个思路了。
于是我找到了google android 文档中,感觉有价值的文档:
这几篇文章写的很详细,让我对Google Play 上架有个基础的概念了。

这张图很关键,我个人理解:上传安装包使用Android App Bundle(AAB)格式,最终是Google 进行apk签名,而不是我们打包时自己签名。这样做有一些好处,防证书丢失,被盗,可以重置证书等,虽然这些好处对我来说目前没有什么作用。
那么如果打包成AAB格式呢?
打包脚本
Flutter flavor 就是 多环境、多版本打包方案。
我这个打包脚本是AI帮我生成的,我就直接贴出来了。只要关注Google Play相关打包就可以了。
我这里android目前包含了3种打包类型
- apk 测试环境的打包。
- apk 生产环境的打包。
- Google Play 生产环境的打包。
与本文相关的只有第3条,为了不遗漏,我把代码完整贴出来,仅供参考。
android/app/build.gradle.kts
kts
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
val isPlayReleaseBuild = gradle.startParameter.taskNames.any {
it.contains("PlayRelease", ignoreCase = true)
}
if (isPlayReleaseBuild) {
val requiredSigningProperties = listOf("storeFile", "storePassword", "keyAlias", "keyPassword")
val missingSigningProperties = requiredSigningProperties.filter {
keystoreProperties.getProperty(it).isNullOrBlank()
}
require(keystorePropertiesFile.exists()) {
"Google Play release signing requires android/key.properties."
}
require(missingSigningProperties.isEmpty()) {
"Missing Google Play signing properties in android/key.properties: ${missingSigningProperties.joinToString()}."
}
require(file(keystoreProperties.getProperty("storeFile")).exists()) {
"Google Play signing storeFile does not exist: ${keystoreProperties.getProperty("storeFile")}."
}
}
android {
namespace = "com.example.boilerplate"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
signingConfigs {
create("playRelease") {
val configuredStoreFile = keystoreProperties.getProperty("storeFile")
if (!configuredStoreFile.isNullOrBlank()) {
storeFile = file(configuredStoreFile)
}
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.boilerplate"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
manifestPlaceholders["appName"] = "YouAppName"
manifestPlaceholders["usesCleartextTraffic"] = "false"
}
flavorDimensions += "environment"
productFlavors {
create("prod") {
dimension = "environment"
manifestPlaceholders["appName"] = "YouAppName"
manifestPlaceholders["usesCleartextTraffic"] = "false"
}
create("play") {
dimension = "environment"
applicationId = "com.company.YouAppName"
manifestPlaceholders["appName"] = "YouAppName"
manifestPlaceholders["usesCleartextTraffic"] = "false"
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".test"
versionNameSuffix = "-test"
manifestPlaceholders["appName"] = "YouAppName Test"
manifestPlaceholders["usesCleartextTraffic"] = "true"
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = if (isPlayReleaseBuild) {
signingConfigs.getByName("playRelease")
} else {
signingConfigs.getByName("debug")
}
}
}
}
flutter {
source = "../.."
}
scripts/build_android_play_prod.sh
bash
#!/usr/bin/env bash
set -euo pipefail
flutter build appbundle --release --flavor play \
--dart-define=APP_ENV=prod \
--dart-define=BASE_URL=https://app.xxxx.com/prod-api
亲测有效,打包生成后可以生成build/app/outputs/bundle/playRelease/app-play-release.aab
然后这个包上传Google Play Console就可以正确的识别了。