第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上下文。
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(主模块) |
|
|
| dynamic_feature_a |
|
|
| shared(基础库) |
|
|
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及基础库之间的依赖关系与清单合并流向:
图中实线箭头表示Gradle直接依赖(runtimeOnly或implementation),虚线箭头表示主模块通过动态模块间接依赖基础库(实际由动态模块直接使用基础库API)。AndroidManifest中的split属性将每个动态模块映射为独立的拆分APK。
3.6 实现流程(分步操作)
- 创建主模块: 使用Android Studio新建一个常规应用项目,包名建议使用
com.example.myapp。 - 添加动态功能模块: 在项目根目录
File → New → New Module → Dynamic Feature Module,向导中设置模块名、最小API和语言。Android Studio会自动生成build.gradle和清单文件模板。 - 配置分发类型: 在动态模块的
build.gradle的android.dynamicFeatures.distribution块中设置deliveryType = "on-demand"。 - 主模块声明动态特性: 在主模块的
build.gradle的android块中添加dynamicFeatures = [":dynamic_feature_a", ":dynamic_feature_b"]。 - 添加依赖: 主模块使用
runtimeOnly project(":dynamic_feature_a")引入动态模块(推荐runtimeOnly以避免编译期直接引用)。 - 验证清单合并: 构建项目后检查
app/build/intermediates/merged_manifests/目录,确认动态模块的dist:module标签已正确合并。 - 同步并构建: 点击
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用法)。
# 查看构建输出
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的“高级功能”分发:
- 在Play Console中新建或选择应用,进入 Release → Setup → Advanced settings。
- 启用 Play Feature Delivery 选项。
- 上传包含动态模块的App Bundle(
.aab)文件,Play Console会自动解析模块列表。 - 设置每个动态模块的分发策略(如需按需下载,则选择“On demand”)。
- 发布内部测试或开放测试,在设备上通过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会自动处理版本协调。