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 测试
相关推荐
草莓熊Lotso1 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
工程师老罗7 小时前
如何在Android工程中配置NDK版本
android
Libraeking11 小时前
破壁行动:在旧项目中丝滑嵌入 Compose(混合开发实战)
android·经验分享·android jetpack
市场部需要一个软件开发岗位11 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
JMchen12313 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs13 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob14 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔14 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei99614 小时前
flutter和Android动画的对比
android·flutter·动画