第3章 创建基础Android项目:项目结构、主模块与动态模块的依赖关系、清单文件配置

动态模块按需加载(Split Install)要求项目在架构上明确分离“主模块”和“动态功能模块”。本章将引导你从零搭建符合Play Feature Delivery(PFD)规范的基础Android项目,重点阐述项目目录结构、模块间的依赖关系以及清单文件的必要配置。只有打好这个基础,后续的on-demand安装和拆分交付才能正确工作。

3.1 原理说明

Play Feature Delivery将应用拆分为一个基础APK(Base APK)和多个配置APK(Configuration APKs)以及动态功能APKs(Dynamic Feature APKs)。基础APK由app主模块构建,包含核心逻辑和所有必需的资源;动态功能APK由独立的Android Library模块构建,在用户运行时按需下载。

关键约束:

  • 主模块必须对动态模块有Gradle依赖(implementation或runtimeOnly),否则打包工具不会将动态模块纳入应用拆分流程。
  • 每个动态模块必须在其build.gradle中声明 distribution 属性,指定交付方式(install-time、on-demand、conditional)。
  • 所有模块(包括主模块和动态模块)的AndroidManifest.xml必须合并,且动态模块的清单需包含dist:module和dist:on-demand等命名空间属性。
  • 动态模块不能独立运行,必须依赖主模块提供的Application和Activity上下文。
提示: 动态模块的代码和资源不能与主模块或其它动态模块发生编译期循环依赖。推荐将公共依赖(如网络库、工具类)提至一个独立的“基础库”模块(如:shared),主模块和动态模块均对该基础库实施依赖。

3.2 项目结构总览

创建基础Android项目后(建议使用Android Studio Arctic Fox或更高版本),项目目录结构应如下:

MyDynamicFeatureApp/
├── app/                              # 主模块(base APK)
│   ├── build.gradle.kts
│   ├── src/main/
│   │   ├── AndroidManifest.xml
│   │   ├── java/
│   │   └── res/
├── dynamic_feature_a/                # 动态模块A(on-demand)
│   ├── build.gradle.kts
│   ├── src/main/
│   │   ├── AndroidManifest.xml
│   │   ├── java/
│   │   └── res/
├── dynamic_feature_b/                # 动态模块B(on-demand)
│   ├── build.gradle.kts
│   ├── src/main/
│   │   ├── AndroidManifest.xml
│   │   └── ...
├── shared/                           # 基础库(可选,建议)
│   ├── build.gradle.kts
│   ├── src/main/
│       ├── AndroidManifest.xml
│       └── java/
├── gradle.properties
├── settings.gradle.kts
└── build.gradle.kts

其中 dynamic_feature_a 和 dynamic_feature_b 是标准的Android Library模块,通过Gradle属性被主模块“感知”。

3.3 模块依赖关系与Gradle配置

模块间的依赖关系必须严格遵守:

  • 主模块必须通过 implementation project(':dynamic_feature_a') 或 runtimeOnly project(':dynamic_feature_a') 依赖动态模块。
  • 动态模块不能反向依赖主模块(但可以通过 provided 或 compileOnly 获取主模块中定义的接口)。
  • 所有模块可共同依赖一个共享基础库(shared),该库应声明为api或implementation。

下表总结了关键配置项:

模块 build.gradle 关键配置 依赖声明示例
app(主模块)
plugins {
    id("com.android.application")
    id("kotlin-android")
}
android {
    ...
    dynamicFeatures = [":dynamic_feature_a", ":dynamic_feature_b"]
}
dependencies {
    implementation(project(":shared"))
    runtimeOnly(project(":dynamic_feature_a"))
    runtimeOnly(project(":dynamic_feature_b"))
}
dynamic_feature_a
plugins {
    id("com.android.dynamic-feature")
    id("kotlin-android")
}
android {
    ...
}
// distribution 配置
android.dynamicFeatures {
    distribution {
        moduleName = "featureA"
        deliveryType = "on-demand"
    }
}
dependencies {
    implementation(project(":app"))
    implementation(project(":shared"))
}
shared(基础库)
plugins {
    id("com.android.library")
    id("kotlin-android")
}
dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
}
警告: 动态模块的 deliveryType 如果设置为 "install-time",则该模块会随着基础APK一同安装,不再具备按需加载的特性。只有设置为 "on-demand" 或 "conditional" 才能实现运行时按需下载。对于“按需加载”课程,请始终使用 on-demand。

3.4 清单文件配置(AndroidManifest.xml)

主模块的清单文件无需特殊修改,但动态模块的清单文件必须包含 <dist:module> 命名空间声明及 split 属性。以下是一个标准配置:

dynamic_feature_a/src/main/AndroidManifest.xml

<?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"
    package="com.example.myapp.dynamic_feature_a"
    split="featureA">

    <dist:module
        dist:onDemand="true"
        dist:title="@string/feature_a_title">
        <dist:fusing dist:include="true" />
    </dist:module>

    <application>
        <activity
            android:name=".FeatureAActivity"
            android:exported="false" />
    </application>
</manifest>

关键属性说明:

  • split="featureA" – 指定拆分名称,Play Console中将其识别为独立的APK。
  • dist:onDemand="true" – 启用按需下载。
  • dist:fusing dist:include="true" – 允许将部分模块内容融合到基础APK中(用于兼容无Play服务的设备)。
  • dist:title – 在安装界面向用户显示的模块名称。

基础库模块(shared)的清单文件可以为空,但建议添加 dist:module 声明并设置 dist:onDemand="false"(因为基础库默认会随主模块安装)。

3.5 模块依赖关系图

下图展示了主模块、动态模块A/B及基础库之间的依赖关系与清单合并流向:

主模块与动态模块依赖关系 App 主模块 动态模块A 动态模块B 基础库(shared) ⚠ AndroidManifest.xml 必须声明 split 和 dist:module

图中实线箭头表示Gradle直接依赖(runtimeOnly或implementation),虚线箭头表示主模块通过动态模块间接依赖基础库(实际由动态模块直接使用基础库API)。AndroidManifest中的split属性将每个动态模块映射为独立的拆分APK。

3.6 实现流程(分步操作)

  1. 创建主模块: 使用Android Studio新建一个常规应用项目,包名建议使用 com.example.myapp。
  2. 添加动态功能模块: 在项目根目录 File → New → New Module → Dynamic Feature Module,向导中设置模块名、最小API和语言。Android Studio会自动生成 build.gradle 和清单文件模板。
  3. 配置分发类型: 在动态模块的 build.gradle 的 android.dynamicFeatures.distribution 块中设置 deliveryType = "on-demand"。
  4. 主模块声明动态特性: 在主模块的 build.gradle 的 android 块中添加 dynamicFeatures = [":dynamic_feature_a", ":dynamic_feature_b"]。
  5. 添加依赖: 主模块使用 runtimeOnly project(":dynamic_feature_a") 引入动态模块(推荐runtimeOnly以避免编译期直接引用)。
  6. 验证清单合并: 构建项目后检查 app/build/intermediates/merged_manifests/ 目录,确认动态模块的 dist:module 标签已正确合并。
  7. 同步并构建: 点击 Sync Now 后执行 ./gradlew :app:assembleDebug,观察输出APK是否被拆分为 base-master.apk 和 featureA-split.apk。

3.7 调试方法

在开发阶段,你可以通过以下方式验证动态模块是否正确构建与拆分:

  • 命令行拆分检查: 使用 aapt2 dump split 查看APK拆分信息。
  • 模拟按需安装: 在设备上安装基础APK后,运行 am install -r -t --dont-kill .../featureA-split.apk 手动部署动态APK进行测试。
  • 使用Play Core API测试: 在代码中调用 SplitInstallManager.startInstall() 并监听状态回调,验证模块下载与注册是否成功(本课程后续章节会详细讲解API用法)。
调试命令示例: 检查拆分后的APK文件列表
# 查看构建输出
ls app/build/outputs/apk/debug/
# 期望输出类似:
# app-debug.apk
# app-debug-split-featureA.apk
# app-debug-split-featureB.apk

3.8 部署步骤

发布到Google Play Store时需要配置Play Console的“高级功能”分发:

  1. 在Play Console中新建或选择应用,进入 Release → Setup → Advanced settings。
  2. 启用 Play Feature Delivery 选项。
  3. 上传包含动态模块的App Bundle(.aab)文件,Play Console会自动解析模块列表。
  4. 设置每个动态模块的分发策略(如需按需下载,则选择“On demand”)。
  5. 发布内部测试或开放测试,在设备上通过Play Store下载验证。

3.9 常见错误处理

错误现象 可能原因 解决方案
构建报错 Module not found: :dynamic_feature_a 主模块未在 settings.gradle.kts 中包含动态模块 在 settings.gradle.kts 中增加 include(":dynamic_feature_a") 并同步
清单合并错误 Attribute "split" not allowed here 动态模块清单未引入 dist 命名空间 在 <manifest> 根标签中添加 xmlns:dist="http://schemas.android.com/apk/distribution"
运行时 SplitInstallException 提示模块未注册 主模块未使用 runtimeOnly 依赖动态模块,或动态模块 distribution 未正确配置 检查主模块 build.gradle 的依赖和 dynamicFeatures 列表
拆分APK未生成 可能使用了 com.android.library 插件而非 com.android.dynamic-feature 修改动态模块的插件为 com.android.dynamic-feature 并删除 applicationId

3.10 性能与稳定性优化

  • 模块粒度控制: 每个动态模块的大小建议不超过20MB,过大的模块会导致下载失败和低端设备内存不足。
  • 依赖精简: 仅将真正需要按需加载的功能放入动态模块,避免将整个库(如RxJava、Gson)全部打包进动态模块,而是通过基础库统一管理公共依赖。
  • 资源混淆与清理: 使用R8/ProGuard时,确保动态模块的资源保持与主模块一致的混淆规则,避免运行时资源ID冲突。
  • 延迟加载策略: 当用户触发按需下载时,优先展示加载UI(如进度提示),避免阻塞主线程。Play Core API提供了 SplitInstallStateUpdatedListener 可实时监听状态。
  • 测试覆盖率: 在staging环境中模拟网络状况差或无Play服务的情况,确保 fusing 回退逻辑正常工作。
最佳实践: 建议将动态模块的 minSdk 设置为与主模块一致(至少21),避免兼容性问题。同时,每个动态模块的 versionCode 与主模块保持一致,Play Console会自动处理版本协调。