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 测试
相关推荐
歪楼小能手12 小时前
Android16系统go版关闭重力旋转开关后缺失手动旋转屏幕悬浮按钮
android·java·平板
崇山峻岭之间13 小时前
Matlab学习记录37
android·学习·matlab
stevenzqzq15 小时前
Android 协程 Channel 菜鸟教程
android·channel
遗悲风15 小时前
PHP伪协议全面解析:原理、常用场景、攻防实战与安全防护
android·安全·php
撩得Android一次心动15 小时前
Android Lifecycle 全面解析:掌握生命周期管理的艺术(源码篇)
android·lifecycle
stevenzqzq15 小时前
android fow 限流
android·限流·flow
冬奇Lab17 小时前
Android 15 显示子系统深度解析(二):图形缓冲区管理与HWC硬件合成
android
wings专栏17 小时前
Android触摸事件分发记录
android
aaajj17 小时前
【Android】声控拍照例子
android
stevenzqzq17 小时前
Android MVI 中 setState(reduce: State.() -> State) 设计说明文档
android·mvi框架