适用场景:什么时候该补这条 Flutter Android 发布链路
如果你的 Flutter Android 项目已经能在真机跑起来,但一到正式发版就开始靠口头传递 keystore、靠截图记版本号、靠“这台电脑能出包”碰运气,那就说明项目还停留在“能开发”,没有进入“能稳定发布”。这篇文章面向的不是零基础入门,而是已经有一个可运行项目,准备把发布能力收口到团队可复用流程里的开发者。若你还在补原生 Android 上传可靠性,可以先看站内的 Android/Kotlin 图片上传实战:用 WorkManager、前台通知和 HTTPS 把失败率降下来,再回到这里把 Flutter 侧的上线闭环补齐。
前置基础:上一篇能力之上,这篇新增什么
默认你已经具备这些前置基础:能用 flutter run 跑通 Android 端、知道 pubspec.yaml 的版本字段、能在 Android Studio 或命令行里执行构建。本文新增的能力不是“如何点击 Build”,而是四件更关键的事:第一,把签名配置从个人电脑习惯变成项目配置;第二,把 JDK、Gradle、AGP 和 Flutter 模板之间的兼容边界说清楚;第三,让 AAB 产物可以被验证、复测和交接;第四,把常见失败日志提前归类,避免上线前临时猜问题。
项目结构:先把发布相关文件放到固定位置
发布能力落地前,先把目录约定统一。推荐至少整理成下面这样:
my_flutter_app/
android/
app/
build.gradle.kts
gradle/
wrapper/
gradle-wrapper.properties
gradle.properties
key.properties
lib/
pubspec.yaml
build/
app/outputs/
这里有两个原则。第一,key.properties 可以存在仓库外,或者只提交模板文件,但项目里必须明确它的读取位置,不要让每个人自己改路径。第二,keystore 本体不要和业务代码混在一起,团队通常会把它放到安全制品库、CI Secret 或受控目录,再通过环境变量或 CI 下载步骤注入。目录清楚之后,后面的配置、命令和日志才有统一上下文。
步骤一:先核对 JDK、Gradle、AGP、Flutter 组合,再谈发版
很多 Flutter Android 发布问题并不是签名坏了,而是底层构建组合已经不兼容。正式发版前,先固定检查命令:
flutter doctor -v
java -version
cd android
./gradlew -version
你至少要把三件事看明白。第一,当前机器到底在用哪个 JDK;第二,Gradle Wrapper 版本是不是项目自己的,而不是 IDE 临时切过去的;第三,升级 Android Studio 后有没有顺带把 AGP 约束抬高。实际排查里,最常见的失败日志之一就是:
Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
这类错误不要先改 Flutter 代码,而是先把 JDK 与 Wrapper 对齐。对多数现役 Flutter Android 项目来说,用 JDK 17 作为发布机基线更稳妥。若你准备尝试 AGP 9,还要额外确认 Flutter 版本和插件是否已经完成 built-in Kotlin 迁移,不要一边升级 IDE,一边把发布链路和插件兼容一起打爆。
如果项目正处在 AGP 9 过渡期,gradle.properties 也要纳入发布检查,而不是只盯着 build.gradle.kts。对尚未完成迁移、尤其是 add-to-app 宿主工程,至少要知道当前是否存在下面这些兼容开关:
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
android.newDsl=false
android.builtInKotlin=false
它们不是“永远正确”的固定答案,而是迁移阶段的稳定器。团队里最危险的情况不是配置旧,而是没人知道自己为什么还留着旧配置。发布前把这些属性和 Flutter 版本、AGP 版本一起记录下来,后面升级时才不会把历史兼容开关误当成正式方案。
步骤二:把签名配置写进工程,而不是留在个人习惯里
先创建 upload keystore,再让 android/app/build.gradle.kts 明确读取它。常用命令如下:
keytool -genkey -v \
-keystore upload-keystore.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias upload
key.properties 可以先写成这样:
storePassword=replace_me
keyPassword=replace_me
keyAlias=upload
storeFile=../secrets/upload-keystore.jks
然后在 android/app/build.gradle.kts 中显式读取配置:
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
}
}
}
这段配置的重点不是“能跑”,而是“能交接”。任何新同事或 CI 节点只要拿到同一份 keystore 与同样的密钥字段,就能得到一致的 release 行为。改完签名配置后,建议立刻执行一次 flutter clean,避免旧缓存让你误判。
步骤三:版本号、构建命令、产物命名要一次收口
不少团队把版本号写在群消息里,把 build 命令记在脑子里,这会让回滚和追包都变得很痛苦。Flutter Android 项目至少要把 pubspec.yaml 的版本号和正式构建命令固定下来,例如:
version: 1.6.0+10600
flutter build appbundle --release --build-name 1.6.0 --build-number 10600
如果你已经区分 staging 与 production,再补上 flavor:
flutter build appbundle --release --flavor production --target lib/main_production.dart
这里的取舍是:日常调试可以继续灵活,正式发布必须收敛。版本号、命令、产物目录、提交 Play Console 的文件类型,都应该在团队文档里只保留一种主路径,避免同一个版本有人传 APK、有人传 AAB,最后连复盘都对不上。
如果你已经接入 CI,也不要让 CI 自己“猜版本”。比较稳妥的做法是由发布人显式传入版本号,或者由 Git Tag 生成,然后统一写入构建日志。最少也要保证下面三项能从日志里直接看出来:这次构建对应哪个 commit、用了哪个 build number、产出了哪个 flavor 的 AAB。否则到了线上回滚时,你连要回滚到哪一包都说不清。
步骤四:发布前验证不能只看“构建成功”
真正的验证至少分三层。第一层是签名验证,先跑:
cd android
./gradlew signingReport
确认 release 变体确实拿到了预期证书,而不是偷偷落回 debug key。第二层是产物验证,构建完成后检查:
build/app/outputs/bundle/release/app-release.aab
如果团队要在提审前先做一次安装验证,可以用 bundletool 把 AAB 转成可安装包,再装到测试机上跑关键路径。第三层是业务验证,至少覆盖登录、首屏、图片加载、支付前入口、崩溃上报初始化这些最容易在 release 里与 debug 表现不同的地方。发布验证阶段一定要保存日志:构建日志、签名报告、测试机安装日志,三类日志缺一类,后面排查都容易变成猜测。
我更建议把 release 冒烟检查写成固定清单,而不是临时想到什么点什么。一个实用版本可以只有五项:应用首次启动是否崩、远端配置或接口证书是否正常、资源压缩后图片与字体有没有缺失、登录态恢复是否正常、崩溃与日志上报 SDK 是否在 release 里成功初始化。Flutter 项目常见的问题是 debug 环境一切正常,release 因混淆、资源收缩或签名环境差异才暴露,所以验证一定要在 release 产物上完成,而不是在 debug 包上“代替验证”。
如果团队已经有测试同学或产品同学参与发版,也建议把这份验证清单做成可勾选的发布卡片,附在每次提审记录里。这样下次线上出问题时,大家能快速知道是“没有验证”,还是“验证过但覆盖面不够”,复盘会更具体。
避坑点:这几类日志看到就别再盲改代码
第一类是 JDK/AGP 不兼容,前面提到的 Java 11 与 AGP 8 冲突就是典型。第二类是签名文件路径错,常见日志类似:
Execution failed for task ':app:validateSigningRelease'
Keystore file '.../upload-keystore.jks' not found
这时先查 storeFile 的相对路径是相对 android/app 还是 android 根目录,不要直接怀疑 Flutter。第三类是密码错误:
Keystore was tampered with, or password was incorrect
这种问题往往来自 CI Secret 写错、换行符污染、或者把 upload key 和 app signing key 概念混在一起。第四类是升级 AGP 9 后插件构建失败。如果日志指向 kotlin-android 或旧 DSL,不要只盯着 pubspec.yaml,还要检查宿主工程和插件的 Gradle 写法是否已经迁移。
第五类避坑点来自“本地能过、CI 失败”。这时优先看两份日志:一份是 CI 节点打印的 java -version 与 ./gradlew -version,一份是 secrets 注入步骤有没有把 keystore 真正落到配置路径。很多团队把问题归因到 Flutter 升级,最后发现只是 CI 容器里没有那份 key.properties,或者工作目录与本地不同,导致相对路径失效。日志先行,比经验判断可靠得多。
复盘清单:一次稳定发布至少要留下什么
- 适用项目、flavor 和目标环境有没有写清楚。
- JDK、Gradle Wrapper、AGP、Flutter 版本有没有记录到发布单。
- keystore 路径、
key.properties字段来源、保管人是否明确。 - 构建命令、产物路径、提交 Play 的文件类型是否唯一。
signingReport、构建日志、测试机安装或启动日志是否归档。- 本次避坑点有没有沉淀成下次可复用的检查步骤。
- CI 节点是否也完成了同样的验证,而不是只在本地电脑验证通过。
- 回滚包、上一个稳定版本号和对应提交记录是否能在发布单里立刻找到。
如果这份复盘清单每次都能补全,发布流程就会逐渐从“谁熟谁上”变成“谁接手都能做”。
下一步进阶方向:从可发布走向可持续发布
把签名和兼容性收口之后,下一篇最值得继续推进的是 CI/CD:把 keystore 注入、flutter build appbundle、版本号策略、灰度发布和回滚检查整合进同一条流水线。到那一步,Flutter Android 团队真正拥有的就不只是一个能运行的 App,而是一条可重复、可审计、可交接的发布系统。