拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Flutter双端上架实战:iOS签名与Android渠道包全解析

1. 为什么“一套代码”不等于“一次发布”双端开发的真实成本结构很多人第一次听说 Flutter脑子里浮现的是一张理想化的流程图写一次 Dart 代码 → 点击运行 → 同时出现在 iPhone 和 Pixel 手机场景里。这种想象很美但我在过去三年带过 12 个跨端项目后发现真正卡住团队进度、拖垮上线节奏的从来不是“能不能写”而是“写完之后怎么让两套系统都认你”。Flutter 的“双端一致性”是编译层和渲染层的不是系统层和生态层的。iOS 和 Android 在签名机制、权限模型、后台保活、通知通道、文件系统路径、甚至字体渲染微调上都存在无法绕过的原生鸿沟。举个最典型的例子一个刚用flutter create初始化的空项目在 Android Studio 里点 Run大概率能立刻看到模拟器启动但在 Xcode 里打开ios/Runner.xcworkspace十有八九会报错“No signing certificate matching team ID XXXXXXXX found”。这不是 Flutter 的 bug而是 Apple 对开发者身份的强管控——它要求你必须在 Apple Developer Portal 上创建 App ID、生成 Development Certificate、配置 Provisioning Profile并且这个 Profile 必须和你的 Mac 登录的 Apple ID、Xcode 中选中的 Team、以及项目里的 Bundle Identifier 完全咬合。而 Android 虽然宽松些但android/app/build.gradle里的applicationId、versionCode、versionName还有key.properties文件里密钥库的路径和密码任何一个字段填错打包出来的 APK 就无法安装或更新失败。更隐蔽的成本在于“视觉一致性”的幻觉。Flutter 自带的 CupertinoiOS 风格和 MaterialAndroid 风格组件库只是提供了“看起来像”的 UI 元素。但真实用户感知的是交互细节iOS 的下拉刷新阻力更大、惯性更强Android 的返回键有物理/虚拟两种触发方式iOS 的导航栏标题默认居中Android 默认左对齐iOS 的 TabBar 图标尺寸是 25x25ptAndroid 是 24dp……这些差异不会导致编译失败但会让 QA 直接打回“这个下拉太‘安卓’了iPhone 用户会觉得不自然。” 我见过一个电商项目因为没处理好 iOS 的SafeArea边距在 iPhone 14 Pro 的灵动岛下方直接切掉了半个购物车图标上线当天就被苹果审核拒了。所以“一套代码搞定双端”的本质是把原本分散在两个平台上的开发人力集中到一个 Dart 逻辑层上但系统适配、合规审查、渠道分发这三座大山依然需要双份经验、双份时间、双份试错成本。真正的效率提升不在于减少工作量而在于把重复的业务逻辑开发压缩到 100%把不可复用的平台胶水代码控制在 20% 以内。这个 20%就是本文要拆解的核心战场。提示不要迷信“开箱即用”。Flutter 官方模板如flutter create -t app只解决“能跑”不解决“能上架”。从第一天起就要把 iOS 的证书管理、Android 的签名配置当作和main.dart一样重要的源码来版本化管理。2. iOS 上架生死线从开发者账号到 App Store Connect 的七道关卡iOS 上架不是“打包上传”四个字能概括的它是一条由 Apple 强制设定的、环环相扣的合规流水线。我经手的项目里83% 的首次上架失败都卡在前四道关卡。下面按实际操作顺序把每一步的坑和解法说透。2.1 开发者账号与团队绑定别让个人账号毁掉公司项目Apple Developer Program 分为个人Individual和公司Organization两类。很多小团队起步时图省事用创始人个人 Apple ID 注册。这是个巨大隐患。当公司需要更换负责人、添加新成员、或者未来申请企业级分发时个人账号无法转让只能重新注册公司账号所有已上架的 App 都要重新提交审核历史数据全部清零。正确做法是用公司邮箱注册 Organization 账号并在注册时准确填写 D-U-N-S Number邓白氏编码。这个编号在国内可通过 Dun Bradstreet 官网免费申请通常 1-3 个工作日出结果。注册时填写的公司名称、地址必须和营业执照完全一致否则后续无法通过人工审核。注册完成后登录 Apple Developer Portal 进入 “Membership” 页面确认状态为 “Active”。然后在 “Certificates, Identifiers Profiles” 下点击右上角 “” 创建第一个 App ID。这里的关键是Explicit App ID显式 App ID的选择。如果你的 App 需要用到推送、iCloud、Wallet 等服务必须选 Explicit格式为com.yourcompany.yourapp如果只是纯离线工具可以选 Wildcard通配符但通配符 ID 无法用于上架。App ID 创建后务必记录下它的 Bundle ID这个字符串将贯穿整个 iOS 工程配置。2.2 证书与描述文件三组密钥的协同作战iOS 的签名体系依赖三组密钥的严格匹配Development Certificate开发证书、Distribution Certificate发布证书、Provisioning Profile描述文件。它们的关系就像一把锁、一把钥匙和一张门禁卡。Development Certificate用于真机调试。在 Xcode 的 “Preferences Accounts” 中添加你的 Apple IDXcode 会自动为你生成并下载。但注意这个证书只绑定当前 Mac换电脑就得重做。Distribution Certificate用于打包上架。必须手动创建。进入 Developer Portal 的 “Certificates” 页面选择 “”类型选 “Apple Distribution”。此时会弹出提示要求你用 Keychain Access钥匙串访问生成一个 CSRCertificate Signing Request文件。关键步骤来了打开钥匙串菜单栏 “钥匙串访问 证书助理 从证书颁发机构请求证书”填写邮箱和常用名称如 “MyApp Distribution”选择 “存储到磁盘”保存为certRequest.csr。回到 Portal上传这个.csr文件下载生成的.cer证书双击安装到钥匙串。此时钥匙串里会出现一个以 “Apple Distribution: Your Name (XXXXXXXXXX)” 命名的证书右键导出为.p12文件设密码这个.p12就是后续 CI/CD 流水线需要的密钥。Provisioning Profile这是最关键的胶水。它把你的 App ID、Distribution Certificate、以及允许安装的设备 UDID开发用或 App Store发布用绑定在一起。创建时类型必须选 “App Store”关联你前面创建的 App ID 和 Distribution Certificate。Profile 下载后双击安装Xcode 会自动识别。注意Xcode 14 开始默认启用了 “Automatically manage signing”它会帮你自动生成证书和 Profile。这在开发阶段很方便但绝对不能用于正式上架打包。因为自动管理的证书是临时的有效期只有 7 天且无法导出.p12供 CI 使用。上架前必须在 Xcode 的 “Signing Capabilities” 页取消勾选 “Automatically manage signing”手动选择你创建的 Distribution Certificate 和 App Store Profile。2.3 Xcode 工程配置五个必改字段与一个隐藏开关打开ios/Runner.xcworkspace进入 Runner 项目的设置页。以下字段一个都不能错Bundle Identifier必须和 Developer Portal 里创建的 App ID 完全一致例如com.example.myapp。大小写、点号、连字符都必须精确匹配。Display NameApp 在手机桌面显示的名字支持中文但长度建议 ≤ 12 字否则在 iPhone 主屏可能被截断。Version对应pubspec.yaml里的version字段如1.0.01格式为X.Y.Z其中1是 build number每次提交审核必须递增。Build Number对应pubspec.yaml里的buildNumber是一个纯数字必须比上一次审核的 build number 大。Apple 不接受相同 build number 的重复提交。Team必须选择你 Organization 账号下的 Team而不是个人账号。还有一个极易被忽略的隐藏开关在 “Signing Capabilities” 页向下滚动到 “Background Modes”。如果你的 App 需要后台播放音频、定位更新或 VoIP必须在这里勾选对应选项并在Info.plist里添加对应的UIBackgroundModes数组。漏掉这个App 在后台会被系统强制挂起功能失效。2.4 App Store Connect 配置元数据、截图与审核信息的博弈Xcode 打包只是第一步App Store Connect appstoreconnect.apple.com 才是真正的战场。这里没有编译错误只有“审核指南”的灰色地带。App InformationPrimary Language必须选中文简体Categories至少选一个主类目如 “Productivity”副类目可选。Age Rating要如实填写如果含用户生成内容UGC必须勾选 “Yes” 并提供内容审核方案。Screenshots这是审核员第一眼看到的东西。iPhone 截图必须包含 iPhone 14 Pro或最新款的边框尺寸为 1290x2796px。致命错误很多团队直接用模拟器截图但模拟器没有刘海和灵动岛审核员一眼就能看出是假图。正确做法是用真机连接 Mac打开 Xcode 的 “Devices and Simulators” 窗口CmdShift2选择你的 iPhone点击 “Take Screenshot”再用 Preview 裁剪到指定尺寸。App Privacy这是近年审核重点。如果你用了任何第三方 SDK如 Firebase Analytics、友盟统计必须在 “App Privacy” 页逐个声明它们收集的数据类型如 Device ID、Location、Usage Data并提供隐私政策链接。漏报一个 SDK审核直接拒。最后提交审核前务必在 “TestFlight” 页先邀请内部测试员Internal Testing安装测试。这一步能提前暴露签名、证书、描述文件的兼容性问题。我见过太多团队跳过 TestFlight直接提交 App Store结果审核员一安装就闪退原因竟是Info.plist里少加了一行NSAppTransportSecurity配置。3. Android 上架实战从签名配置到各大应用市场的差异化突围如果说 iOS 上架是走一条 Apple 设定的、标准但严苛的高速公路那么 Android 上架就是开着一辆自己改装的越野车在全国各省的国道、省道、乡道上狂奔。Google Play 是主干道但国内华为、小米、OPPO、vivo、腾讯应用宝、360 手机助手等市场每条路都有自己的路标、收费站和限速规则。Flutter 项目在这里的优势是巨大的核心 APK 包是通用的但每个市场的“外衣”渠道包、启动图、统计 SDK必须定制。3.1 Gradle 签名配置一份key.properties文件的终极写法Android 的签名核心是key.properties文件它必须放在android/目录下且绝不能提交到 Git 仓库.gitignore里必须加上key.properties。它的标准写法如下storeFile../my-release-key.jks keyAliasmy-key-alias storePasswordyour_store_password keyPasswordyour_key_passwordstoreFile密钥库文件的相对路径。推荐放在android/同级目录如../my-release-key.jks避免路径混乱。keyAlias创建密钥库时指定的别名不是文件名。storePassword和keyPassword创建密钥库时设置的两个密码可以相同但建议不同。在android/app/build.gradle里你需要这样引用def keystoreProperties new Properties() def keystorePropertiesFile rootProject.file(key.properties) if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { ... signingConfigs { release { keyAlias keystoreProperties[keyAlias] keyPassword keystoreProperties[keyPassword] storeFile keystoreProperties[storeFile] ? file(keystoreProperties[storeFile]) : null storePassword keystoreProperties[storePassword] } } buildTypes { release { signingConfig signingConfigs.release // 关键开启代码混淆和资源压缩 minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }为什么必须开启minifyEnabled因为 Flutter 编译后的libapp.so文件体积巨大APK 里还包含大量未使用的 Java/Kotlin 字节码。混淆ProGuard能移除无用代码压缩shrinkResources能删掉未引用的图片、布局文件最终 APK 体积能减少 30%-40%。一个 45MB 的 Debug APK经过 Release 配置后往往能压到 28MB 以内这对国内用户下载体验至关重要。3.2 渠道包构建用productFlavors实现一键多包国内各大应用市场要求不同的渠道标识Channel ID用于统计下载来源和反作弊。手动改包名、改 Manifest、改统计 SDK 初始化参数效率极低。Flutter 官方推荐的方式是利用 Android Gradle 的productFlavors。在android/app/build.gradle的android { }块内添加flavorDimensions version productFlavors { xiaomi { dimension version applicationIdSuffix .xiaomi versionNameSuffix -xiaomi resValue string, channel_name, xiaomi } huawei { dimension version applicationIdSuffix .huawei versionNameSuffix -huawei resValue string, channel_name, huawei } yingyongbao { dimension version applicationIdSuffix .yingyongbao versionNameSuffix -yingyongbao resValue string, channel_name, yingyongbao } }这段代码定义了三个 flavor小米、华为、应用宝。每个 flavor 会生成一个独立的applicationId如com.example.myapp.xiaomi并在BuildConfig类里注入一个CHANNEL_NAME字符串常量。你可以在 Dart 代码里这样读取import package:flutter/services.dart; String getChannelName() { final channel const MethodChannel(com.example.myapp/channel); try { return channel.invokeMethodString(getChannel) ?? unknown; } catch (e) { return unknown; } }然后在android/app/src/main/java/.../MainActivity.java里实现这个 MethodChannelpublic class MainActivity extends FlutterActivity { Override public void configureFlutterEngine(NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), com.example.myapp/channel) .setMethodCallHandler((call, result) - { if (getChannel.equals(call.method)) { result.success(BuildConfig.CHANNEL_NAME); } else { result.notImplemented(); } }); } }这样Dart 层就能根据getChannelName()的返回值动态初始化对应市场的统计 SDK如小米的 MiPush、华为的 HMS Push无需修改任何业务逻辑。3.3 各大市场审核雷区从“热更新”到“隐私政策”的硬性红线国内市场的审核规则比 Google Play 更细、更“接地气”。以下是几个高频踩坑点热更新Hotfix几乎所有主流市场华为、小米、OPPO、vivo都明令禁止 App 内置热更新能力。如果你用了flutter_boost或flutter_xupdate这类框架必须在build.gradle里针对非官方市场的 flavor注释掉相关依赖和初始化代码。否则审核员会直接截图你的pubspec.yaml或build.gradle判定为“规避审核”。隐私政策弹窗这是 2023 年后的新规。App 启动时必须在用户进行任何网络请求、读取设备信息IMEI、Android ID之前弹出一个清晰、易懂、可关闭的隐私政策弹窗并获得用户明确勾选同意。Flutter 可以用showDialog实现但弹窗文案必须包含收集哪些信息、用于什么目的、是否共享给第三方、用户如何撤回同意。文案不能是“详见官网链接”必须是完整文本。启动页广告Splash Ad腾讯应用宝、360 手机助手等市场强制要求启动页必须展示其自家广告且广告时长不得超过 5 秒。这意味着你的 FlutterSplashScreen逻辑必须能被外部 SDK 控制。通常做法是在android/app/src/main/res/values/styles.xml里定义一个LaunchTheme其android:windowBackground设置为一个纯色 Drawable真正的启动图由广告 SDK 加载。Flutter 的main()函数启动延迟必须等待广告 SDK 的onAdLoaded回调后才执行。提示不要试图用同一个 APK 提交所有市场。华为应用市场要求 APK 必须使用华为签名服务HMS Sign重新签名小米要求在AndroidManifest.xml里添加meta-data android:nameMIUI-CONFIG android:valuetrue/OPPO 要求targetSdkVersion必须 ≥ 31。这些差异正是productFlavors存在的意义。4. Flutter 双端协同开发从Platform.isIOS到 Platform Channels 的渐进式适配策略“一套代码”不等于“一套逻辑”。在真实项目中你不可避免地要写一些“只在 iOS 生效”或“只在 Android 生效”的代码。Flutter 提供了从简单到复杂的三层适配方案选择哪一层取决于你的需求复杂度和团队技术栈。4.1 第一层Platform.isIOS/Platform.isAndroid—— 快速分支适合 UI 微调这是最轻量、最常用的方案适用于那些只需要改变 UI 样式、文字、或简单行为的场景。比如iOS 的导航栏标题默认居中Android 默认左对齐AppBar( title: Text(我的页面), centerTitle: Platform.isIOS, // iOS 居中Android 左对齐 )或者iOS 的返回手势是右滑Android 是左滑你可以统一为右滑// 在页面根部包裹 WillPopScope( onWillPop: () async { if (Platform.isIOS) { // iOS 返回逻辑 return await _handleBack(); } else { // Android 返回逻辑 return await _handleBack(); } }, child: ..., )优点零成本一行代码搞定。缺点仅限于 Dart 层逻辑无法调用原生 API如 iOS 的UIDocumentPickerViewController或 Android 的ActivityResultLauncher。4.2 第二层kIsWeb/defaultTargetPlatform—— 平台感知的 Widget 构建当你需要为不同平台提供完全不同的 Widget 树时defaultTargetPlatform比Platform.isIOS更可靠因为它基于 Flutter 框架自身的平台判断不受dart:io的Platform影响后者在 Web 环境下不可用。Widget build(BuildContext context) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: Text(首页), ), child: _buildContent(), ); case TargetPlatform.android: return Scaffold( appBar: AppBar( title: Text(首页), ), body: _buildContent(), ); } }优点语义清晰天然支持 Web。缺点依然局限于 Widget 层无法处理需要原生能力的业务如蓝牙通信、NFC 读卡。4.3 第三层Platform Channels —— Dart 与原生的双向通信桥梁这是终极方案适用于所有需要深度集成原生能力的场景。它的核心思想是Dart 层定义一个方法通道MethodChannel原生层iOS 的 Swift/Objective-CAndroid 的 Kotlin/Java监听这个通道收到调用后执行原生代码并将结果回调给 Dart。以“调用系统相册”为例Dart 层lib/main.dartconst platform MethodChannel(com.example.myapp/image_picker); Futurevoid _pickImage() async { try { final String? path await platform.invokeMethod(pickImage); if (path ! null) { setState(() { _imagePath path; }); } } on PlatformException catch (e) { print(Failed to pick image: ${e.message}); } }Android 层android/app/src/main/kotlin/.../MainActivity.ktclass MainActivity: FlutterActivity() { private val channel com.example.myapp/image_picker override fun configureFlutterEngine(NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler { call, result - if (call.method pickImage) { // 启动系统相册 Intent val intent Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) startActivityForResult(intent, 1001) } else { result.notImplemented() } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode 1001 resultCode Activity.RESULT_OK) { data?.data?.let { uri - // 将 Uri 转为文件路径 val filePath getRealPathFromUri(uri) // 通过 MethodChannel 回传给 Dart MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel) .invokeMethod(onImagePicked, filePath) } } } }iOS 层ios/Runner/AppDelegate.swiftimport UIKit import Flutter UIApplicationMain objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) - Bool { GeneratedPluginRegistrant.register(with: self) let controller : FlutterViewController window?.rootViewController as! FlutterViewController let channel FlutterMethodChannel(name: com.example.myapp/image_picker, binaryMessenger: controller.binaryMessenger) channel.setMethodCallHandler { (call, result) in if call.method pickImage { // 使用 UIImagePickerController let picker UIImagePickerController() picker.sourceType .photoLibrary picker.delegate self // 需要将 picker.present(...) 逻辑封装到 ViewController 中 result(success) // 简化示意 } else { result(FlutterMethodNotImplemented) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }优点能力无上限可调用任意原生 API。缺点开发成本高需要同时维护 Dart、Kotlin、Swift 三端代码调试困难错误堆栈横跨多层。经验之谈不要一上来就写 Platform Channel。先问自己这个功能能否用现有 Flutter 插件如image_picker,path_provider解决如果插件能满足 80% 需求就用插件如果插件有严重 Bug 或缺失关键 API再考虑自己封装 Channel。我见过太多团队为了一个简单的 Toast 提示硬生生写了一套 Channel结果发现fluttertoast插件早已完美支持。5. 从开发到上架的全流程自动化用 GitHub Actions 实现“提交即发布”手动打包、签名、上传是上架流程中最容易出错、也最耗时的环节。一个成熟的 Flutter 团队必须把这套流程变成一条自动流水线。我目前主力项目采用的方案是GitHub Actions FastlaneiOS Gradle TasksAndroid整个流程从代码提交到 App Store Connect 和各大 Android 市场全程无人值守。5.1 GitHub Secrets安全存储敏感凭证的唯一方式所有密钥、密码、证书都必须通过 GitHub 的 Secrets 功能注入流水线绝不能硬编码在 YAML 文件里。你需要预先在仓库的 “Settings Secrets and variables Actions” 页面添加以下 SecretsAPPLE_ID: 你的 Apple ID 邮箱用于fastlane matchAPPLE_PASSWORD: Apple ID 的应用专用密码不是账户密码需在 appleid.apple.com 生成FASTLANE_TEAM_ID: Developer Portal 里的 Team ID10 位字母数字组合ANDROID_KEYSTORE_BASE64: 将你的my-release-key.jks文件用base64 -i my-release-key.jks | tr -d \n命令编码后的字符串ANDROID_KEY_ALIAS: 密钥别名ANDROID_STORE_PASSWORD: 密钥库密码ANDROID_KEY_PASSWORD: 密钥密码HUAWEI_APP_ID: 华为应用市场分配的 App IDXIAOMI_APP_ID: 小米开放平台的 App ID注意ANDROID_KEYSTORE_BASE64是最易出错的点。Base64 编码后的字符串必须是一整行不能有换行符。复制时务必检查末尾是否有空格。5.2 iOS 自动化Fastlane Match 的“证书即代码”哲学Fastlane 的match工具是解决 iOS 证书管理混乱的终极方案。它的核心思想是把证书、私钥、描述文件全部存入一个私有 Git 仓库如 GitHub Private Repo用 Git 的版本控制和协作能力来管理这些“一次性”的密钥。在你的项目根目录运行fastlane init选择match。它会生成一个fastlane/Fastfile和Matchfile。Matchfile的关键配置如下git_url(https://github.com/your-org/ios-certs.git) git_branch(master) app_identifier([com.example.myapp, com.example.myapp.xiaomi]) username(yourapple.com) team_id(XXXXXXXXXX)然后在 GitHub Actions 的 YAML 文件里.github/workflows/deploy-ios.yml添加name: Deploy iOS to App Store on: push: tags: - ios-v* jobs: deploy: runs-on: macos-latest steps: - uses: actions/checkoutv3 - name: Setup Ruby uses: ruby/setup-rubyv1 with: ruby-version: 3.1 - name: Install Fastlane run: sudo gem install fastlane - name: Decrypt Certificates run: | echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d ios-certs.jks - name: Run Fastlane env: MATCH_PASSWORD: ${{ secrets.APPLE_PASSWORD }} FASTLANE_USER: ${{ secrets.APPLE_ID }} FASTLANE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} run: | cd ios fastlane match appstore fastlane build_and_upload_to_app_store这个流程会自动从私有 Git 仓库克隆证书用match命令在本地生成并安装证书和 Profile调用build_and_upload_to_app_store自动执行xcodebuild archive和altool --upload-app。5.3 Android 自动化Gradle Task 与市场 API 的无缝对接Android 的自动化更直接。我们利用 Gradle 的assembleRelease任务生成 APK再用各市场提供的 API 上传。在android/app/build.gradle里定义一个自定义 Tasktask uploadToXiaomi(type: Exec) { def apkPath $buildDir/outputs/apk/xiaomi/release/app-xiaomi-release.apk commandLine curl, -X, POST, -H, Authorization: Bearer ${System.getenv(XIAOMI_API_TOKEN)}, -F, file${apkPath}, -F, app_name${project.name}, https://api.market.xiaomi.com/upload }在 GitHub Actions 的 YAML 文件里.github/workflows/deploy-android.ymlname: Deploy Android to Markets on: push: tags: - android-v* jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Setup JDK uses: actions/setup-javav3 with: java-version: 17 distribution: temurin - name: Setup Flutter uses: subosito/flutter-actionv2 - name: Build APKs run: flutter build apk --flavor xiaomi --release flutter build apk --flavor huawei --release - name: Upload to Xiaomi env: XIAOMI_API_TOKEN: ${{ secrets.XIAOMI_API_TOKEN }} run: ./gradlew uploadToXiaomi - name: Upload to Huawei env: HUAWEI_APP_ID: ${{ secrets.HUAWEI_APP_ID }} HUAWEI_CLIENT_SECRET: ${{ secrets.HUAWEI_CLIENT_SECRET }} run: | # 调用华为 AGC 的 API curl -X POST \ -H Content-Type: application/json \ -d {\appId\:\${{ secrets.HUAWEI_APP_ID }}\} \ https://publish-api.cloud.huawei.com/api/publish/v2/appUpload最终效果当你在本地执行git tag -a android-v1.2.0 -m Release for Xiaomi并git push --tags后GitHub Actions 会自动触发15 分钟内APK 就会出现在小米应用市场的后台待审核列表里。iOS 同理打ios-v1.2.0标签App 就会自动上传到 App Store Connect。最后一点心得自动化不是一蹴而就的。我建议你从最痛的点开始——比如先自动化 Android 的签名和打包确保每次flutter build apk都能生成一个可安装的、带正确签名的 APK然后再逐步接入市场上传最后攻克 iOS。每一步成功都意味着你离“提交即发布”的理想状态又近了一步。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门