🔧Flutter 3.24.x项目AGP环境升级,支持Android 15(API 级别 35)

0 阅读4分钟

fj.jpg

🚨 政策背景

自 Google Play 政策调整以来,应用必须在 2025 年 11 月 1 日前适配 Android 15(API 级别 35),否则将无法发布更新。本文结合实际 Flutter 项目,详解 Android 端升级适配流程与实战技巧,适用于 Flutter 3.24.x 版本开发者。

Google Play政策通知

应用必须以 Android 15(API 级别 35 或更高级别为目标平台

状态 您将无法发布应用更新(还剩 120 天)

发送日期 2025年7月2日

最后期限 2025年11月1日 -   延期已获批

为了向用户提供安全可靠的使用体验,Google Play 要求所有应用都必须符合目标 API 级别要求。

 2025年11月1日起,如果您的目标 API 级别不是在最新的 Android 版本发布日期前 1 年内推出的,您将无法更新您的应用。

🛠️ 升级准备工作

1. 下载更新 Android Studio 版本

  • 指定版本: Android Studio Meerkat Feature Drop | 2024.3.2 Patch 1 May 28, 2025
  • Android Studio 历史版本下载地址: developer.android.com/studio/arch…
  • 注意 Java 版本依赖 Android Studio 自带版本

2. 打开 Android Studio 下载相关依赖

  1. 打开 File -> Settings -> Languages & Frameworks -> Android SDK
  2. SDK Platforms 勾选 Android 15.0
  3. SDK Tools 勾选相关依赖,注意勾选 Show Package Details
  4. 勾选完成后,点击 Apply,下载安装

由于各自项目不同,具体依赖项下载,提供截图,作为参考项:

SDK Platforms 配置

SDK Tools 配置1

SDK Tools 配置2

SDK Tools 配置3

🔧 项目代码配置实战

1. 修改 android/settings.gradle

确保插件版本、依赖源正确,并加入新版 AGP 和 Flutter 插件支持:

plugins {
    id "com.android.application" version '8.6.0' apply false
    id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}

2. 修改 android/build.gradle

为解决 namespace 适配问题,加入自动设置 namespace 的逻辑,适配 AGP 8.x 版本的变动。

完整脚本略长,可参考附录中的完整版代码(支持自定义 namespace 或从 AndroidManifest.xml 自动提取)。


3. 修改 gradle-wrapper.properties

指定使用 Gradle 8.7:

distributionUrl=https://services.gradle.org/distributions/gradle-8.7-all.zip

4. 修改 android/app/build.gradle

重点关注配置项:

android {
    compileSdk 35
    ndkVersion "26.3.11579264"

    defaultConfig {
        ...
        minSdk = 23
        targetSdkVersion 35
    }
}

注意根据项目需要设置 productFlavors、签名信息、Proguard 等参数。


5. 添加 proguard-rules.pro 文件

为防止 R8 误删类,可加入以下规则:

-keep class * extends android.app.Service
-dontwarn com.google.android.gms.**
-dontwarn com.google.android.play.core.**

更多规则详见 StackOverflow 提供的 R8 混淆修复方案。

🔧 完整项目代码配置(仅供参考)

1. android/settings.gradle 文件修改

pluginManagement {
    def flutterSdkPath = {
        def properties = new Properties()
        file("local.properties").withInputStream { properties.load(it) }
        def flutterSdkPath = properties.getProperty("flutter.sdk")
        assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
        return flutterSdkPath
    }
    settings.ext.flutterSdkPath = flutterSdkPath()

    includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version '8.6.0' apply false
    id "com.google.gms.google-services" version "4.3.15" apply false
    id "com.google.firebase.crashlytics" version "2.8.1" apply false
    id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}

include ":app"

2. android/build.gradle 文件修改,主要适配 namespace 问题

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}
subprojects(project -> {
    project.afterEvaluate(subproject -> {
        if (subproject.getPlugins().hasPlugin("com.android.library") || subproject.getPlugins().hasPlugin("com.android.application")) {
            try {
                Object android;
                if (subproject.getPlugins().hasPlugin("com.android.library")) {
                    android = subproject.getExtensions().getByType(com.android.build.gradle.LibraryExtension.class);
                } else {
                    android = subproject.getExtensions().getByType(com.android.build.gradle.AppExtension.class);
                }

                String namespace = (String) android.getClass().getMethod("getNamespace").invoke(android);
                System.out.println("item " + subproject.getName() + " Current Namespace: " + namespace);

                if (namespace == null || namespace.isEmpty()) {
                    String packageName = null;

                    if (subproject.getPlugins().hasPlugin("com.android.application")) {
                        packageName = (String) android.getClass().getMethod("getDefaultConfig")
                                .invoke(android)
                                .getClass()
                                .getMethod("getApplicationId")
                                .invoke(android.getClass().getMethod("getDefaultConfig").invoke(android));
                    }

                    if (packageName == null || packageName.isEmpty()) {
                        File manifestFile = (File) android.getClass()
                                .getMethod("getSourceSets")
                                .invoke(android)
                                .getClass()
                                .getMethod("getByName", String.class)
                                .invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main")
                                .getClass()
                                .getMethod("getManifest")
                                .invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main"))
                                .getClass()
                                .getMethod("getSrcFile")
                                .invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main").getClass().getMethod("getManifest").invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main")));

                        if (manifestFile.exists()) {
                            String manifestText = new String(java.nio.file.Files.readAllBytes(manifestFile.toPath()));
                            java.util.regex.Pattern packagePattern = java.util.regex.Pattern.compile("package=\"([^\"]*)\"");
                            java.util.regex.Matcher matchResult = packagePattern.matcher(manifestText);
                            if (matchResult.find()) {
                                packageName = matchResult.group(1);
                            }
                        }
                    }

                    if (packageName == null || packageName.isEmpty()) {
                        packageName = !subproject.getGroup().toString().equals("unspecified")
                                ? subproject.getGroup().toString()
                                : "com.auto.generated." + subproject.getName();
                    }

                    android.getClass().getMethod("setNamespace", String.class).invoke(android, packageName);
                    System.out.println("Namespace set: " + packageName + " (item: " + subproject.getName() + ")");

                    File manifestFile = (File) android.getClass()
                            .getMethod("getSourceSets")
                            .invoke(android)
                            .getClass()
                            .getMethod("getByName", String.class)
                            .invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main")
                            .getClass()
                            .getMethod("getManifest")
                            .invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main"))
                            .getClass()
                            .getMethod("getSrcFile")
                            .invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main").getClass().getMethod("getManifest").invoke(android.getClass().getMethod("getSourceSets").invoke(android).getClass().getMethod("getByName", String.class).invoke(android.getClass().getMethod("getSourceSets").invoke(android), "main")));

                    if (manifestFile.exists()) {
                        String manifestText = new String(java.nio.file.Files.readAllBytes(manifestFile.toPath()));
                        if (manifestText.contains("package=")) {
                            String updatedManifestText = manifestText.replaceAll("package=\"[^\"]*\"", "");
                            java.nio.file.Files.write(manifestFile.toPath(), updatedManifestText.getBytes());
                            System.out.println("AndroidManifest.xml del package attribute (item: " + subproject.getName() + ")");
                        } else {
                            System.out.println("AndroidManifest.xml not found package attribute (item: " + subproject.getName() + ")");
                        }
                    } else {
                        System.out.println("Not found AndroidManifest.xml file (item: " + subproject.getName() + ")");
                    }
                }
            } catch (Exception e) {
                System.out.println("set item " + subproject.getName() + " namespace error: " + e.getMessage());
            }
        }
    });
});
rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

tasks.register("clean", Delete) {
    delete rootProject.buildDir
}

3. android/gradle/wrapper/gradle-wrapper.properties 文件修改

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-8.7-all.zip

4. android/app/build.gradle 文件修改

  • 注意下面是我项目的配置,参考修改
  • 主要是 compileSdk、ndkVersion、targetSdkVersion 修改
plugins {
    id "com.android.application"
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    namespace "com.xxxxx"
    compileSdk 35
    ndkVersion "26.3.11579264"

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        applicationId "com.xxxx"
        minSdk = 23
        targetSdkVersion 35
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    flavorDimensions "default"
    productFlavors {
        prod {
            dimension "default"
            resValue "string", "app_name", "xxx"
        }
        dev {
            dimension "default"
            applicationIdSuffix ".test"
            resValue "string", "app_name", "xxx"
        }
        uat {
            dimension "default"
            applicationIdSuffix ".uat"
            resValue "string", "app_name", "xxx"
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.4.1'
}

5. 增加 android/app/proguard-rules.pro 文件

-keep class * extends android.app.Service
-dontwarn com.google.android.gms.auth.api.credentials.Credential$Builder
-dontwarn com.google.android.gms.auth.api.credentials.Credential
-dontwarn com.google.android.gms.auth.api.credentials.CredentialPickerConfig$Builder
-dontwarn com.google.android.gms.auth.api.credentials.CredentialPickerConfig
-dontwarn com.google.android.gms.auth.api.credentials.CredentialRequest$Builder
-dontwarn com.google.android.gms.auth.api.credentials.CredentialRequest
-dontwarn com.google.android.gms.auth.api.credentials.CredentialRequestResponse
-dontwarn com.google.android.gms.auth.api.credentials.Credentials
-dontwarn com.google.android.gms.auth.api.credentials.CredentialsClient
-dontwarn com.google.android.gms.auth.api.credentials.HintRequest$Builder
-dontwarn com.google.android.gms.auth.api.credentials.HintRequest
-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
-dontwarn com.google.android.play.core.splitinstall.SplitInstallException
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest
-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState
-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
-dontwarn com.google.android.play.core.tasks.OnFailureListener
-dontwarn com.google.android.play.core.tasks.OnSuccessListener
-dontwarn com.google.android.play.core.tasks.Task

💻 实际开发环境参考

Mac 电脑相关环境

[✓] Flutter (Channel stable, 3.24.3, on macOS 15.3 24D60 darwin-arm64, locale zh-Hans-CN)
    • Flutter version 3.24.3 on channel stable at /Users/xxx/development/flutterUpstream repository https://github.com/flutter/flutter.gitFramework revision 2663184aa7 (10 个月前), 2024-09-11 16:27:48 -0500Engine revision 36335019a8Dart version 3.5.3DevTools version 2.37.3

[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    • Android SDK at /Users/xxx/Library/Android/sdkPlatform android-35, build-tools 35.0.1Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/javaJava version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
    • Xcode at /Applications/Xcode.app/Contents/DeveloperBuild 16B40CocoaPods version 1.16.2

[✓] Chrome - develop for the webChrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2024.3)
    • Android Studio at /Applications/Android Studio.app/ContentsFlutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutterDart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dartJava version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)

[✓] VS Code (version 1.101.1)
    • VS Code at /Applications/Visual Studio Code.app/ContentsFlutter extension version 3.112.0

Windows 电脑相关环境

[√] Flutter (Channel stable, 3.24.3, on Microsoft Windows [版本 10.0.22631.5335], locale zh-CN)
    • Flutter version 3.24.3 on channel stable at D:\SDK\flutter\3.24.3Upstream repository https://github.com/flutter/flutter.gitFramework revision 2663184aa7 (10 months ago), 2024-09-11 16:27:48 -0500Engine revision 36335019a8Dart version 3.5.3DevTools version 2.37.3

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    • Android SDK at C:\Users\xxxx\AppData\Local\Android\sdkPlatform android-35, build-tools 35.0.1Java binary at: D:\Program Files\Android\Android Studio\jbr\bin\javaJava version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)
    • All Android licenses accepted.

[√] Chrome - develop for the webChrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Android Studio (version 2024.3.2)
    • Android Studio at D:\Program Files\Android\Android StudioFlutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutterDart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dartJava version OpenJDK Runtime Environment (build 21.0.6+-13368085-b895.109)

🔍 常见问题与踩坑点

  • ⚠️ Java JDK 建议统一使用 Android Studio 自带版本,避免环境差异
  • ⚠️ 部分插件可能不兼容 AGP 8.6,请逐个排查升级
  • ⚠️ namespace 问题一定要配置完整,AGP 8.x 强制要求所有 module 显式声明 namespace
  • ⚠️ Proguard 配置要提前准备,避免发布后崩溃或丢类

📚 参考资料

  1. Flutter Gradle Plugin 变更说明
  2. Android 15 适配指南
  3. Flutter 项目 Android 15 升级实践
  4. AGP 升级助手
  5. Android 15 新特性解析
  6. R8 混淆配置问题
  7. AGP 8.4.0 版本说明
  8. Flutter Stripe 插件 Android 配置

✅ 总结

本次适配是一次涉及 AGP、Gradle、Flutter 构建体系的大版本升级,建议团队逐项排查并记录升级脚本,便于后续版本演进。

如本文对你有所帮助,欢迎点赞、评论交流,或者收藏方便后续查阅 🫡