Android 多模块构建无法解析 :payment variant
结论
:payment 虽然被声明为 Dynamic Feature,实际使用的却是 com.android.application 插件。因此,它只发布了普通应用模块的 ApiElements 和 RuntimeElements variant,没有发布基础模块需要的 android-reverse-meta-data variant。
正确配置需要满足以下条件:
:app使用com.android.application。:payment使用com.android.dynamic-feature。:app通过dynamicFeatures += ":payment"注册动态功能模块。- 删除
:app对:payment的普通implementation依赖。 - 在
: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 才能匹配并解析该模块。
备注:内容仅供参考。
