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 测试
相关推荐
无言Echo2 小时前
Android 高斯模糊(1) 窗口模糊及java侧基本流程简述
android
无言Echo2 小时前
Android 高斯模糊(2)BackgroundBlurDrawable使用及相关Bug
android
清蒸鳜鱼2 小时前
【Open-AutoGLM】MacOS+Android配置、使用指南
android·macos
唐叔在学习2 小时前
buildozer打包详解:细说那些我踩过的坑
android·后端·python
2501_946233893 小时前
Flutter与OpenHarmony帖子详情页面开发
android·java·flutter
冬奇Lab3 小时前
稳定性性能系列之四——异常日志机制与进程冻结:问题排查的黑匣子
android·性能优化·车载系统·bug
秋邱3 小时前
Java匿名内部类的使用场景:从语法本质到实战优化全解析
android·java·开发语言·数据库·python
Kapaseker3 小时前
凌晨两点磨锋芒 调试采坑莫慌张
android·kotlin
2501_924064114 小时前
2025年移动应用渗透测试流程方案及iOS与Android框架对比
android·ios