4、创建动态功能模块:在项目中添加feature模块
前面几章我们聊了动态交付的原理和整体配置。这一章,咱们动手干点实在的——在项目里真正创建一个 feature 模块。
说白了,feature 模块就是你的功能单元。它跟普通 Android Library 最大的区别是什么?它可以被单独打包、按需交付。用户不需要的功能,咱就不给他下载。我刚开始接触这个的时候,觉得它跟 library 没啥两样,后来踩了几个坑才明白——差别大了去了。
核心理解:feature 模块 = 独立功能 + 按需交付 + 生命周期自管理。它不是普通 library,它是可插拔的功能包。
4.1 理解 feature 模块的角色
在动态交付体系里,feature 模块就是那个「可拆分的零件」。你想想看,一个完整的 App 就像一辆汽车,发动机、座椅、天窗都是可选的 feature。用户选了天窗版本,咱才把天窗的代码和数据发给他。
feature 模块有几个关键特征:
- 独立性:它可以单独编译、单独打包。
- 按需性:只有用户触发了功能入口,才触发下载安装。
- 自包含:模块自己的界面、资源、逻辑都放在内部。
- 依赖公共层:它通常会依赖一个 base 模块,但这个依赖是编译期的。
我个人习惯把 feature 模块比作「微应用」。每个 feature 都有自己的 Activity、Fragment、ViewModel,只不过它们共享了 base 层的网络、数据库、公共组件。
那它跟普通 library 到底有啥区别?我整理了一张表:
| 对比维度 | 普通 Library 模块 | Feature 模块 |
|---|---|---|
| 打包方式 | 随主包一起打包 | 独立打包,按需下发 |
| 交付时机 | 安装时全部交付 | 使用到才交付 |
| 模块配置 | 普通 library 配置 | 需要配置 dist 和 split 属性 |
| 生命周期 | 随 App 启动 | 由 SplitInstallManager 管理 |
| 卸载能力 | 无法单独卸载 | 可以按需卸载 |
小提示:不要把公共组件放在 feature 模块里。feature 模块之间不能互相依赖——否则就失去了「按需交付」的意义了。
4.2 创建 feature 模块的完整步骤
好,咱们现在开始实操。假设你已经在 Android Studio 里打开了项目。创建 feature 模块分四步走:
- 新建模块:右键项目根目录 → New → New Module → Android Library。
- 命名规范:我建议统一用
feature_<功能名>的格式,比如feature_payment、feature_search。 - 配置 build.gradle:添加动态交付相关的插件和属性。
- 配置清单文件:添加
<dist>和<split>标签。
第二步里的命名规范,我多说一句。我在项目中遇到过团队用 :payment 这种短名字的,结果项目大了之后根本分不清哪个是 feature、哪个是 library。统一前缀真的能救命。
下面是 build.gradle 的核心配置:
// feature_payment/build.gradle
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
// 注意:这里不需要 com.android.dynamic-feature 插件
}
android {
namespace 'com.example.feature.payment'
compileSdk 33
defaultConfig {
minSdk 21
// 不要设置 applicationId
}
// 开启动态交付功能
dynamicFeatures = []
}
dependencies {
// 依赖公共基础库
implementation project(':base')
// 依赖 app 模块中的公共代码(非必须)
implementation project(':app')
}
然后,最重要的一步——配置清单文件。这是 feature 模块和普通 library 模块最明显的差异点:
<!-- feature_payment/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.feature.payment">
<dist:module
dist:instant="false"
dist:title="@string/module_payment_title">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
<application>
<activity
android:name=".PaymentActivity"
android:exported="false" />
</application>
</manifest>
注意:dist:module 标签里的 dist:title 必须引用字符串资源,不能写硬编码字符串。不然编译会报错。我曾经因为这个查了整整一个下午。
这里有几个关键属性你需要知道:
- dist:on-demand:标记这个模块是按需下载的。用户不点进来,它就不存在。
- dist:fusing dist:include="true":允许这个模块被融合到基础安装包中。说白了,如果用户手机不支持动态交付,这个模块还能退化成普通 library 打进主包里。
- dist:instant:是否是 instant App。一般设 false 就行。
4.3 管理模块间的依赖关系
创建完 feature 模块之后,依赖关系得理清楚。我见过不少新手把依赖搞成循环,编译直接炸掉。
咱们的依赖原则就三条:
- app 模块依赖 feature 模块——但这里的依赖是「动态依赖」,不是编译期强制依赖。
- feature 模块依赖 base 模块——这是编译期依赖,公共代码放 base 里。
- feature 模块之间不能互相依赖——这一点非常重要!一旦有了互相依赖,你就没法单独按需交付了。
在 app 模块的 build.gradle 里,配置动态依赖的方式是这样的:
// app/build.gradle
dependencies {
// 普通编译依赖
implementation project(':base')
// 动态功能模块依赖——使用 dynamicFeatures 声明
dynamicFeatures = [':feature_payment', ':feature_search']
}
嗯,注意这里的写法。它不是 implementation project,而是用 dynamicFeatures 数组来声明。这个数组告诉 Gradle:这些模块是动态交付的,不要把它们的代码打进主包。
我个人的做法:在 feature 模块里用 api project(':base') 而不是 implementation。这样 feature 模块暴露出来的接口里如果引用了 base 的类型,调用方不用再单独依赖 base。当然,这个看项目规范,没有对错。
4.4 代码组织与包结构
功能模块的代码组织,直接关系到后续维护成本。我踩过坑之后,形成了一套固定的模式:
feature_payment/
├── src/main/
│ ├── java/com/example/feature/payment/
│ │ ├── di/ # 依赖注入
│ │ │ └── PaymentModule.kt
│ │ ├── ui/ # 界面层
│ │ │ ├── PaymentActivity.kt
│ │ │ └── PaymentFragment.kt
│ │ ├── viewmodel/ # 业务逻辑
│ │ │ └── PaymentViewModel.kt
│ │ ├── data/ # 数据层
│ │ │ ├── PaymentRepository.kt
│ │ │ └── PaymentApi.kt
│ │ └── PaymentNavigator.kt # 模块入口
│ └── res/
│ ├── layout/
│ └── values/
└── build.gradle
每个 feature 模块都是一个独立的小宇宙。它有自己的 UI、ViewModel、数据层。唯一共享的,是 base 模块里的网络客户端、数据库实例、公共工具类。
入口设计上,我习惯用一个 Navigator 或者 Router 类来暴露模块的入口 Intent。这样 app 模块只需要知道这个 Navigator 类,不需要了解模块内部的具体实现:
// PaymentNavigator.kt
object PaymentNavigator {
fun createIntent(context: Context, orderId: String): Intent {
return Intent(context, PaymentActivity::class.java).apply {
putExtra("order_id", orderId)
}
}
}
app 模块里调用的时候,只需要通过 SplitInstallManager 确保模块已安装,然后直接启动 Intent 就行:
// 在 app 模块中启动 feature 模块
val intent = PaymentNavigator.createIntent(this, "order_123")
startActivity(intent)
4.5 避坑指南——我曾遇到的问题
这一节全是实战中淌出来的经验。你仔细看,能省好几个小时的调试时间。
- 模块安装成功但启动崩溃?我曾经遇到一个诡异的问题——feature 模块里的资源文件引用不到。后来发现是
dist:fusing配了false,导致模块在某些设备上资源加载失败。解决方案:dist:fusing一定要设true。 - 编译报错 "Module not found"?检查一下
settings.gradle里是否 include 了 feature 模块路径。我忘了加过至少三次。 - 构建产物里没有 feature 的 apk?这是因为你没有在
app/build.gradle的dynamicFeatures数组里声明。只创建模块不声明,等于白干。 - 模块间共享数据导致耦合?千万别让 feature 模块直接访问另一个 feature 模块的类。如果非要共享数据,走 base 模块里的接口或者事件总线。
终极提醒:feature 模块的清单文件中,package 不能和 app 模块或者其他 feature 模块重复。否则安装时会提示冲突。我就因为这个被 QA 打回来过两次……
4.6 小结一下
创建 feature 模块这件事,说白了就是三步:新建模块 → 配好 dist 标签 → 声明动态依赖。但每一步都有细节,漏一个就编译不过。
我个人觉得,动态交付最迷人的地方不是技术本身,而是它逼着你把项目架构理清楚。模块边界、依赖方向、入口设计——这些在普通项目里经常被忽略的东西,在动态交付体系里,一个都逃不掉。
好,这一章就到这儿。下面这张图总结了 feature 模块在项目中的位置和关系,你可以收藏着用。