Dynamic Feature Modules 笔记

🧩 一、前提条件

  • 主应用已启用 App Bundle(即使用 .aab 发布)
  • build.gradle (Project) 中使用 Android Gradle Plugin 3.2+
  • 在主模块的 build.gradle 中启用了动态交付支持(通常默认开启)
less 复制代码
// app/build.gradle
android {
    dynamicFeatures = [":dynamic_feature"] // 声明动态模块
}

🛠️ 二、创建 Dynamic Feature Module

步骤 1:在 Android Studio 中创建模块

  1. 菜单栏:File > New > New Module

  2. 选择 Dynamic Feature Module

  3. 配置:

    • Module name : dynamic_feature
    • Package name : com.example.myapp.dynamic_feature
    • Minimum API level: 与主模块一致(建议 ≥ 21)
    • Title: "高级工具"(可选,用于 Play 商店展示)
  4. 点击 Finish

✅ 此操作会自动生成一个带 SplitCompat 支持的模块,并在主模块的 dynamicFeatures 中自动注册(若未自动添加,请手动添加如上所示)。


📄 三、编写动态模块内容

1. 创建 Activity(DynamicActivity.kt

kotlin 复制代码
// dynamic_feature/src/main/java/com/example/myapp/dynamic_feature/DynamicActivity.kt
package com.example.myapp.dynamic_feature

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.myapp.dynamic_feature.databinding.ActivityDynamicBinding

class DynamicActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDynamicBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityDynamicBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.textView.text = "这是动态加载的功能模块!"
        binding.buttonBack.setOnClickListener { finish() }
    }
}

2. 添加布局文件(activity_dynamic.xml

ini 复制代码
<!-- dynamic_feature/src/main/res/layout/activity_dynamic.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp" />

    <Button
        android:id="@+id/buttonBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="返回" />
</LinearLayout>

3. 在 AndroidManifest.xml 中声明 Activity

xml 复制代码
<!-- dynamic_feature/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

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

</manifest>

⚠️ 注意:不要设置 android:exported="true",除非你明确需要外部启动。


🔌 四、在主模块中请求安装并启动动态模块

1. 添加依赖(通常已自动添加)

确保主模块的 build.gradle 包含:

arduino 复制代码
implementation 'com.google.android.play:core-ktx:1.8.1' // 推荐使用 KTX
// 或基础版
implementation 'com.google.android.play:core:1.10.3'

2. 编写加载逻辑(例如在 MainActivity 中)

kotlin 复制代码
// app/src/main/java/com/example/myapp/MainActivity.kt
package com.example.myapp

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.PackageInfoCompat
import com.example.myapp.databinding.ActivityMainBinding
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
import com.google.android.play.core.splitinstall.SplitInstallRequest
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var splitInstallManager: SplitInstallManager
    private lateinit var listener: SplitInstallStateUpdatedListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        splitInstallManager = SplitInstallManagerFactory.create(this)

        // 监听安装状态
        listener = SplitInstallStateUpdatedListener { state ->
            when (state.status()) {
                SplitInstallSessionStatus.INSTALLED -> {
                    Toast.makeText(this, "模块安装成功!", Toast.LENGTH_SHORT).show()
                    startDynamicActivity()
                }
                SplitInstallSessionStatus.FAILED -> {
                    Toast.makeText(this, "模块安装失败: ${state.errorCode()}", Toast.LENGTH_LONG).show()
                }
                SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
                    // 可选:处理需要用户确认的情况(如大体积模块)
                    splitInstallManager.startConfirmationDialogForResult(
                        state,
                        this,
                        REQ_CODE_CONFIRMATION
                    )
                }
            }
        }

        binding.buttonLoadFeature.setOnClickListener {
            loadAndLaunchDynamicFeature()
        }
    }

    private fun loadAndLaunchDynamicFeature() {
        if (isDynamicFeatureInstalled()) {
            startDynamicActivity()
        } else {
            val request = SplitInstallRequest.newBuilder()
                .addModule("dynamic_feature") // 模块名必须与 build.gradle 中的模块名一致
                .build()

            splitInstallManager.registerListener(listener)
            splitInstallManager.startInstallation(request)
        }
    }

    private fun isDynamicFeatureInstalled(): Boolean {
        return splitInstallManager.installedModules.contains("dynamic_feature")
    }

    private fun startDynamicActivity() {
        try {
            val intent = Intent().setClassName(
                packageName,
                "com.example.myapp.dynamic_feature.DynamicActivity"
            )
            startActivity(intent)
        } catch (e: Exception) {
            Toast.makeText(this, "无法启动动态模块", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        splitInstallManager.unregisterListener(listener)
        super.onDestroy()
    }

    companion object {
        private const val REQ_CODE_CONFIRMATION = 1001
    }
}

3. 添加按钮到主界面布局

xml 复制代码
<!-- app/src/main/res/layout/activity_main.xml -->
<Button
    android:id="@+id/buttonLoadFeature"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="加载动态功能" />

🧪 五、本地测试方法

由于 .aab 不能直接安装,需用 bundletool 生成 APK 并安装:

ini 复制代码
# 1. 构建 aab
./gradlew bundleDebug

# 2. 生成 apks(需签名)
java -jar bundletool.jar build-apks --bundle=app/build/outputs/bundle/debug/app-debug.aab --output=app.apks --mode=default

# 3. 安装到设备
java -jar bundletool.jar install-apks --apks=app.apks

如果你只是调试,也可以在 Android Studio Run 时选择 "Deploy as instant app" 或使用 Internal App Sharing 快速测试。


📦 六、发布注意事项

  • 动态模块会随主 App 一起上传到 Google Play Console
  • 用户首次使用该功能时才会下载对应模块(节省初始安装体积)
  • 模块大小建议 ≤ 10MB,否则可能触发用户确认弹窗
  • 可结合 on-demand delivery (按需)、conditional delivery (条件分发)、instant delivery(免安装体验)使用

✅ 总结

步骤 内容
1 创建 Dynamic Feature Module
2 编写模块内的 Activity 和资源
3 主模块通过 Play Core Library 请求安装
4 安装成功后通过类名启动 Activity
5 使用 bundletool 或 Play Console 测试
相关推荐
Kapaseker39 分钟前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴1 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android