适用场景:什么时候该补这条 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

如果你已经区分 stagingproduction,再补上 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,而是一条可重复、可审计、可交接的发布系统。