PAYATHON 2026

Android 多模块构建无法解析 :payment variant

支付老李

结论

:payment 虽然被声明为 Dynamic Feature,实际使用的却是 com.android.application 插件。因此,它只发布了普通应用模块的 ApiElements 和 RuntimeElements variant,没有发布基础模块需要的 android-reverse-meta-data variant。

正确配置需要满足以下条件:

  1. :app 使用 com.android.application。
  2. :payment 使用 com.android.dynamic-feature。
  3. :app 通过 dynamicFeatures += ":payment" 注册动态功能模块。
  4. 删除 :app 对 :payment 的普通 implementation 依赖。
  5. 在 :payment 中添加对基础模块 :app 的依赖。

Dynamic Feature 的依赖方向如下:

app --注册--> payment
payment --代码依赖--> app

不能同时把 payment 当作普通 Android Library 或普通 Application 使用。

为什么会出现 variant 不匹配

报错中最值得关注的是这一行:

consumer needed a component for use during 'android-reverse-meta-data'

基础应用处理 Dynamic Feature 时,需要从功能模块获取专用的反向元数据。对应的 variant 由 Android Dynamic Feature 插件创建。

目前 :payment 使用的是:

alias(libs.plugins.android.application)

所以它只会产生类似下面的普通应用 variant:

debugApiElements
debugRuntimeElements
releaseApiElements
releaseRuntimeElements

这些 variant 分别用于编译期或运行期,无法满足 android-reverse-meta-data 请求。Gradle 并不是在提示 debug variant 不存在,而是在说明已有 variant 的用途属性与请求不兼容。

手动指定下面的配置也无法创建缺失的 Dynamic Feature variant:

implementation(project(path = ":payment", configuration = "default"))

这种写法还绕过了正常的 variant-aware dependency resolution,应当删除。

修改版本目录中的插件声明

如果项目使用 libs.versions.toml 管理插件,需要为 Dynamic Feature 添加别名,并确保它与项目现有的 Android Gradle Plugin 使用相同版本。

例如:

[versions]
agp = "8.8.1"

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-dynamic-feature = { id = "com.android.dynamic-feature", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

如果插件统一声明在根构建脚本中,也可以这样配置:

plugins {
    id("com.android.application") version "8.8.1" apply false
    id("com.android.dynamic-feature") version "8.8.1" apply false
    id("org.jetbrains.kotlin.android") version "..." apply false
}

Kotlin 版本应沿用项目已有配置,不要照着示例随意修改。

:payment 的正确配置

将 android.application 替换为 android.dynamic.feature,再让功能模块依赖基础模块:

plugins {
    alias(libs.plugins.android.dynamic.feature)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.stream.payment"
    compileSdk = 35

    defaultConfig {
        minSdk = 27

        testInstrumentationRunner =
            "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            proguardFiles("proguard-rules.pro")
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }

    kotlinOptions {
        jvmTarget = "21"
    }

    buildFeatures {
        compose = true
        aidl = false
        buildConfig = false
        renderScript = false
        shaders = false
    }
}

dependencies {
    implementation(project(":app"))

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)

    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)

    testImplementation(libs.junit)

    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)

    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

还要注意以下配置:

  • Dynamic Feature 不应声明独立的 applicationId。
  • 功能模块通常不需要单独设置 targetSdk,应用级配置由基础模块提供。
  • 示例将 minSdk 调整为与 :app 相同的 27。功能模块不应无意中把最低系统版本提高到 28。如果确实只打算向较高版本的 Android 系统交付该功能,应使用 Dynamic Feature 的交付条件,而不是依赖两个模块之间的 minSdk 差异。
  • Dynamic Feature 可以访问基础模块公开的代码,因此依赖要写在 :payment 中。如果共享代码不适合放在应用模块,可以将其抽取到独立的 Android Library 模块。

如果版本目录中没有对应别名,也可以直接使用插件 ID:

plugins {
    id("com.android.dynamic-feature")
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

:app 的正确配置

:app 继续使用 Application 插件,并保留 Dynamic Feature 注册:

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}

android {
    namespace = "com.stream.myapp"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.stream.myapp"
        minSdk = 27
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner =
            "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }

    kotlinOptions {
        jvmTarget = "21"
    }

    buildFeatures {
        compose = true
    }

    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }

    dynamicFeatures += setOf(":payment")
}

dependencies {
    implementation(libs.androidx.navigation.compose)

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)

    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.androidx.material.icons.extended)

    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.ktx)
    ksp(libs.androidx.room.compiler)

    testImplementation(libs.junit)

    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    androidTestImplementation(libs.androidx.navigation.testing)

    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

必须从 :app 中删除这一行:

implementation(
    project(path = ":payment", configuration = "default")
)

也不要改成普通项目依赖:

implementation(project(":payment"))

基础应用不会通过普通 implementation 消费 Dynamic Feature。dynamicFeatures DSL 已经建立了基础应用和功能模块之间的特殊关系。

settings.gradle.kts

当前的模块注册方式没有问题,可以简化为:

pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(
        RepositoriesMode.FAIL_ON_PROJECT_REPOS
    )

    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "My App"

include(":app")
include(":payment")

正则字符串需要留意。要在 Kotlin 字符串中让正则点号表示字面量,应写成:

"androidx\\..*"

也可以使用原始字符串:

"""androidx\..*"""

不过,仓库过滤配置并不是这次 android-reverse-meta-data 错误的根因。

检查 Dynamic Feature 的 Manifest

:payment/src/main/AndroidManifest.xml 还必须是合法的功能模块 Manifest。例如,按需安装模块可以使用下面的结构:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <dist:module
        dist:instant="false"
        dist:title="@string/payment_module_title">

        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>

        <dist:fusing dist:include="true" />
    </dist:module>

    <application />
</manifest>

具体交付方式要根据需求选择:

  • 安装时交付:dist:install-time
  • 按需交付:dist:on-demand
  • 条件交付:在 dist:install-time 下配置相应条件

Manifest 中的交付配置不能替代 com.android.dynamic-feature 插件,两部分都要正确配置。

如果 payment 并不需要动态安装

如果只是想拆分代码,不需要通过 App Bundle 独立交付或按需安装,就不该使用 Dynamic Feature。此时应将 :payment 配置为普通 Android Library。

:payment:

plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

:app:

android {
    // 删除 dynamicFeatures 配置
}

dependencies {
    implementation(project(":payment"))
}

这种模式的依赖方向是:

app -> payment library

Dynamic Feature 模式则由 payment 依赖 app,基础模块只通过 dynamicFeatures 注册它。两种模式只能选择一种,不能混用。

插件和依赖方向调整完成后,com.android.dynamic-feature 会为 :payment 创建专用 variant,其中包括反向元数据。这样,:app:debugReverseMetadataValues 才能匹配并解析该模块。

备注:内容仅供参考。