4. 创建第一个动态功能模块

这节课,我们来亲手创建第一个动态功能模块。你想想看,整个动态交付的核心就是这些“随用随取”的模块。怎么做?我带你一步步拆解。

4.1 模块长什么样?先搭架子

在 Android Studio 里,我习惯直接在项目根目录下建一个 features 文件夹(别笑,后来很多项目都跟着我这么干),然后在里面创建动态功能模块。方法很简单:右键项目 → New → Module → Android Dynamic Feature Module。填个名字,比如 feature_video,完事。

创建好之后,你会看到这样一个结构:

app/                            // 基础模块(Base)
features/
  feature_video/
    src/main/
      java/                    // 代码
      res/                     // 资源
      AndroidManifest.xml      // 模块清单
    build.gradle               // 模块构建脚本

嗯,这里要注意:feature_video 并不是一个独立的 App,它必须挂载在某个“宿主”上。说白了,它就像是 App 的一个可拆卸部件。

4.2 build.gradle 配置:关键一步

模块创建完,第一件事就是配 build.gradle。我个人习惯先看一眼默认生成的,再手动改。

先看 base 模块(app) 的 build.gradle —— 它需要引用所有动态模块:

// app/build.gradle
android {
    ...
    dynamicFeatures = [":features:feature_video"]
}
dependencies {
    // 其他依赖...
    // 注意:不要在base里直接implementation动态模块的代码
}
千万别手贱在 base 模块里 direct implementation 动态模块的库,否则模块会被打包进 APK,动态交付就白做了。我当年就踩过这个坑——包体直接多了 20MB。

再看 feature 模块 的 build.gradle:

// features/feature_video/build.gradle
apply plugin: 'com.android.dynamic-feature'

android {
    compileSdk 33
    defaultConfig {
        minSdk 21
    }
}
dependencies {
    implementation project(':app')   // 必须依赖 base 模块
    // 其他 feature 特有的依赖
}

插件用了 com.android.dynamic-feature 而不是 com.android.application 或 com.android.library。这就是告诉 Gradle:“兄弟,我是一块动态砖,哪里需要哪里搬。”

4.3 feature 模块的 AndroidManifest 配置

接下来是清单文件。每个动态模块的 AndroidManifest.xml 里必须写一个 <dist:module> 标签,并指定 split 属性。这个属性的值就是模块的安装标识,将来在代码中请求安装时要用到它。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.example.feature_video">

    <dist:module
        dist:instant="false"
        dist:title="@string/feature_video_title">
        <dist:delivery>
            <dist:on-demand />
        </dist:delivery>
        <dist:fusing dist:include="true" />
    </dist:module>

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

这里几个关键点:

  • dist:on-demand:按需下载,用户不点就不下。
  • fusing include="true":允许这个模块在安装包被拆包时,也跟主包融合(即旧设备或没有 Play Core 的情况下还能用)。
  • dist:title:模块的显示名称,会在下载通知或界面上看到。
  • split 属性:虽然上面没写,但默认 split 值就是模块的路径名(例如 feature_video)。你也可以显式指定:dist:split="myVideo",这样在请求安装时就要用这个字符串。
我个人习惯给 split 属性起一个简短、唯一的名字,比如 video 或 ar_core。不要用模块全路径,否则代码里写起来太啰嗦。

4.4 一张图看懂整体结构

我把上面三步串起来,画了一张流程结构图。你看它就像盖房子——先打地基(base),再搭砖块(feature模块),最后用清单告诉系统怎么搬砖。

动态功能模块创建流程 ① 创建Feature Module (如 features/feature_video) ② 配置 build.gradle 动态功能插件 + 依赖 ③ 清单文件 dist:module + split ④ Base模块引用 dynamicFeatures = [...] ⑤ SplitInstall请求 按 split 名称下载安装 ✅ 动态交付模块就绪 按需下载,不增加安装包体积 注意:每个动态模块必须有自己的 split 名称,且不能与其它模块重复

4.5 配置清单中的坑(我曾经踩过的)

讲几个我亲身经历的坑:

  • split 名称冲突:如果你有两个模块都叫 feature_common,那动态交付会直接崩溃。我不止一次看见同事把两个不同 repo 的模块起了同样的 split 名。解决方案:规范命名,比如 com.yourcompany.feature.video。
  • fusing 不配置:如果 dist:fusing include="false",那么当你把 APK 拆分成多个 split 文件时,这个模块不会打包进基础 APK,旧设备直接无法运行。建议始终设为 true,除非你有非常特殊的场景。
  • 清单中缺少 dist:module 标签:Gradle aapt 不会报错,但安装时模块不会被识别为动态模块,导致 SplitInstallManager 找不到它。检查清单的最简单方法:编译后查看 build/intermediates/manifest 下的合并结果。

4.6 小结:一个简单的对照表

配置项 位置 作用
dynamicFeatures app/build.gradle 声明哪些模块是动态的
com.android.dynamic-feature 插件 feature/build.gradle 告诉 Gradle 这是动态模块
<dist:module> feature/AndroidManifest.xml 定义模块的分发属性
dist:split feature/AndroidManifest.xml 标识模块的唯一名称
dist:fusing feature/AndroidManifest.xml 是否允许与主包融合

嗯,到现在为止,你的模块骨架就搭起来了。接下来就是往 feature 里填业务代码,再配合 SplitInstallManager 触发下载。这些我们后面几章慢慢聊。