配置按需分发

借助功能模块,您可以从应用的基本模块中分离某些功能和资源,并将其纳入到 app bundle 中。然后,用户在安装应用的基本 APK 后,便可以通过 Play Feature Delivery 按需下载和安装这些组件。

例如,假设某个短信应用包含拍摄和发送图片消息的功能,但只有一小部分用户发送图片消息。合理的做法是,将图片消息功能添加为可下载的功能模块。这样,所有用户最初下载的应用所占的空间会更小,而只有那些发送图片消息的用户才需要下载该附加组件。

请注意,这种类型的模块化所需的工作量更大,并且可能需要重构应用的现有代码,因此请仔细考虑按需提供给用户对应用的哪些功能益处最大。如需更好地了解有关按需功能的最佳用例和准则,请阅读有关按需分发的用户体验最佳做法

如果您想逐步对应用功能进行模块化处理,而不启用按需分发等高级分发选项,请改为配置安装时分发

本文旨在帮助您向应用项目中添加功能模块,并将其配置为按需分发。开始前,请确保您使用的是 Android Studio 3.5 或更高版本以及 Android Gradle 插件 3.5.0 或更高版本。

将新模块配置为按需分发

如需创建新功能模块,最简单的方法是使用 Android Studio 3.5 或更高版本。由于功能模块本身依赖于应用的基础模块,因此您只能在创建好应用项目之后再向其中添加功能模块。

如需使用 Android Studio 向应用项目中添加功能模块,请按以下步骤操作:

  1. 在 IDE 中打开您的应用项目(如果您尚未打开)。
  2. 从菜单栏中依次选择 File(文件)> New(新建)> New Module(新建模块)
  3. Create New Module(创建新模块)对话框中,选择 Dynamic Feature Module(动态功能模块),然后点击 Next(下一步)。
  4. Configure your new module(配置新模块)部分中,完成以下操作:
    1. 从下拉菜单中选择应用项目的 Base application module(应用基础模块)。
    2. 指定 Module name(模块名称)。IDE 会使用此名称在 Gradle 设置文件中将该模块标识为 Gradle 子项目。当您构建 app bundle 时,Gradle 会使用子项目名称的最后一个元素在功能模块的清单中注入 <manifest split> 属性。
    3. 指定该模块的 package name(软件包名称)。默认情况下,Android Studio 会给出一个软件包名称建议,该名称由基础模块的根软件包名称和您在上一步中指定的模块名称组合而成。
    4. 选择您希望该模块支持的 Minimum API level(最低 API 级别)。此值应与基础模块的值一致。
  5. 点击 Next(下一步)。
  6. Module Download Options(模块下载选项)部分中,完成以下操作:

    1. 指定最多包含 50 个字符的 Module title。例如,平台会在确认用户是否要下载模块时,使用此标题向用户标识该模块。因此,应用的基本模块必须将模块标题作为您能读懂的字符串资源纳入其中。使用 Android Studio 创建模块时,IDE 会为您将字符串资源添加到基本模块中,并在功能模块的清单中注入以下条目:

      <dist:module
          ...
          dist:title="@string/feature_title">
      </dist:module>
      
    2. Install-time inclusion 下的下拉菜单中,选择 Do not include module at install-time。Android Studio 会在该模块的清单中注入以下内容来反映您的选择:

      <dist:module ... >
        <dist:delivery>
            <dist:on-demand/>
        </dist:delivery>
      </dist:module>
      
    3. 如果您希望此模块可提供给搭载 Android 4.4(API 级别 20)及更低版本的设备并包含在多 APK 中,请选中 Fusing 旁边的复选框。这意味着,您可以为此模块启用按需行为,也可以停用融合功能,以便从不支持下载和安装拆分 APK 的设备上将其省略。Android Studio 会在该模块的清单中注入以下内容来反映您的选择:

      <dist:module ...>
          <dist:fusing dist:include="true | false" />
      </dist:module>
      
  7. 点击 Finish(完成)。

在 Android Studio 完成模块创建后,从 Project(项目)窗格(从菜单栏中依次选择 View [查看] > Tool Windows [工具窗口] > Project[项目])中自行检查其内容。默认代码、资源和组织应与标准应用模块类似。

接下来,您需要使用 Play Feature Delivery 库实现按需安装功能。

将 Play Feature Delivery 库添加到您的项目中

开始之前,您需要先将 Play Feature Delivery 库添加到您的项目中。

请求按需模块

当您的应用需要使用功能模块时,它可以通过 SplitInstallManager 类在前台进行请求。在发起请求时,您的应用需要指定由目标模块清单中的 split 元素所定义的模块名称。当您使用 Android Studio 创建功能模块时,构建系统会使用您提供的模块名称,在编译时将该属性注入模块清单中。如需了解详情,请参阅功能模块清单

例如,假设某个具有按需模块的应用可使用设备的相机拍摄和发送图片消息,并且此按需模块在其清单中指定了 split="pictureMessages"。以下示例使用 SplitInstallManager 请求 pictureMessages 模块(以及包含一些宣传过滤器的另一个模块):

Kotlin

// Creates an instance of SplitInstallManager.
val splitInstallManager = SplitInstallManagerFactory.create(context)

// Creates a request to install a module.
val request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build()

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener { sessionId -> ... }
    .addOnFailureListener { exception ->  ... }

Java

// Creates an instance of SplitInstallManager.
SplitInstallManager splitInstallManager =
    SplitInstallManagerFactory.create(context);

// Creates a request to install a module.
SplitInstallRequest request =
    SplitInstallRequest
        .newBuilder()
        // You can download multiple on demand modules per
        // request by invoking the following method for each
        // module you want to install.
        .addModule("pictureMessages")
        .addModule("promotionalFilters")
        .build();

splitInstallManager
    // Submits the request to install the module through the
    // asynchronous startInstall() task. Your app needs to be
    // in the foreground to submit the request.
    .startInstall(request)
    // You should also be able to gracefully handle
    // request state changes and errors. To learn more, go to
    // the section about how to Monitor the request state.
    .addOnSuccessListener(sessionId -> { ... })
    .addOnFailureListener(exception -> { ... });

当您的应用请求按需模块时,Play Feature Delivery 库会采用“即发即弃”策略。也就是说,它会发送请求以将该模块下载到平台,但不会监控安装是否成功。如需在安装后继续用户体验历程或妥善处理错误,请务必监控请求状态

注意:您可以请求已安装在设备上的功能模块。如果检测到该模块已安装,API 会立即将该请求视为已完成。此外,安装模块后,Google Play 会自动使其保持最新状态。也就是说,当您上传新版 app bundle 时,平台会更新所有属于您应用的已安装 APK。如需了解详情,请参阅管理应用更新

如需访问模块的代码和资源,您的应用需要启用 SplitCompat。请注意,Android 免安装应用不需要使用 SplitCompat。

延迟安装按需模块

如果您不需要应用立即下载并安装按需模块,可以延迟到应用在后台运行时再安装该模块。例如,您想要预先加载一些宣传材料并在之后应用启动时使用这些材料。

您可以使用 deferredInstall() 方法指定之后需要下载的模块,如下所示。而且,与 SplitInstallManager.startInstall() 不同,您的应用无需在前台就可发起延迟安装请求。

Kotlin

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(listOf("promotionalFilters"))

Java

// Requests an on demand module to be downloaded when the app enters
// the background. You can specify more than one module at a time.
splitInstallManager.deferredInstall(Arrays.asList("promotionalFilters"));

收到延迟安装请求后,系统将尽力而为,您无法跟踪其进度。因此,在尝试访问您已指定为延迟安装的模块之前,应检查该模块是否已安装。如果您需要立即使用该模块,请改为使用 SplitInstallManager.startInstall() 进行请求,如上一部分中所示。

监控请求状态

为了能够更新进度条、在安装后触发 Intent 或者妥善处理请求错误,您需要监听来自异步 SplitInstallManager.startInstall() 任务的状态更新。在开始接收安装请求更新之前,请先注册监听器并获取该请求的会话 ID,如下所示。

Kotlin

// Initializes a variable to later track the session ID for a given request.
var mySessionId = 0

// Creates a listener for request status updates.
val listener = SplitInstallStateUpdatedListener { state ->
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
}

// Registers the listener.
splitInstallManager.registerListener(listener)

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener { sessionId -> mySessionId = sessionId }
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener { exception ->
        // Handle request errors.
    }

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener)

Java

// Initializes a variable to later track the session ID for a given request.
int mySessionId = 0;

// Creates a listener for request status updates.
SplitInstallStateUpdatedListener listener = state -> {
    if (state.sessionId() == mySessionId) {
      // Read the status of the request to handle the state update.
    }
};

// Registers the listener.
splitInstallManager.registerListener(listener);

...

splitInstallManager
    .startInstall(request)
    // When the platform accepts your request to download
    // an on demand module, it binds it to the following session ID.
    // You use this ID to track further status updates for the request.
    .addOnSuccessListener(sessionId -> { mySessionId = sessionId; })
    // You should also add the following listener to handle any errors
    // processing the request.
    .addOnFailureListener(exception -> {
        // Handle request errors.
    });

// When your app no longer requires further updates, unregister the listener.
splitInstallManager.unregisterListener(listener);

处理请求错误

请注意,按需安装功能模块有时可能会失败,就像应用安装并非总能成功一样。安装失败可能是设备存储空间不足、没有网络连接或用户未登录 Google Play 商店等问题所致。如需了解有关如何从用户的角度妥善处理这些情况的建议,请参阅按需分发的用户体验指南

在代码方面,您应该使用 addOnFailureListener() 处理下载或安装模块时出现的失败,如下所示:

Kotlin

splitInstallManager
    .startInstall(request)
    .addOnFailureListener { exception ->
        when ((exception as SplitInstallException).errorCode) {
            SplitInstallErrorCode.NETWORK_ERROR -> {
                // Display a message that requests the user to establish a
                // network connection.
            }
            SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> checkForActiveDownloads()
            ...
        }
    }

fun checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .sessionStates
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                // Check for active sessions.
                for (state in task.result) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        }
}

Java

splitInstallManager
    .startInstall(request)
    .addOnFailureListener(exception -> {
        switch (((SplitInstallException) exception).getErrorCode()) {
            case SplitInstallErrorCode.NETWORK_ERROR:
                // Display a message that requests the user to establish a
                // network connection.
                break;
            case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
                checkForActiveDownloads();
            ...
    });

void checkForActiveDownloads() {
    splitInstallManager
        // Returns a SplitInstallSessionState object for each active session as a List.
        .getSessionStates()
        .addOnCompleteListener( task -> {
            if (task.isSuccessful()) {
                // Check for active sessions.
                for (SplitInstallSessionState state : task.getResult()) {
                    if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                        // Cancel the request, or request a deferred installation.
                    }
                }
            }
        });
}

下表介绍了您的应用可能需要处理的错误状态:

错误代码 说明 建议采取的措施
ACTIVE_SESSIONS_LIMIT_EXCEEDED 请求遭到拒绝,因为当前至少有一个请求正在下载。 检查是否有任何仍在下载的请求,如上例所示。
MODULE_UNAVAILABLE Google Play 无法根据当前安装的应用版本、设备和用户的 Google Play 账号找到所请求的模块。 如果用户无权访问该模块,请通知他们。
INVALID_REQUEST Google Play 已收到请求,但该请求无效。 验证请求中包含的信息是否完整准确。
SESSION_NOT_FOUND 找不到指定会话 ID 对应的会话。 如果您尝试通过会话 ID 监控请求的状态,请确保会话 ID 正确无误。
API_NOT_AVAILABLE 当前设备不支持 Play Feature Delivery 库。也就是说,该设备无法按需下载和安装功能。 对于搭载 Android 4.4(API 级别 20)或更低版本的设备,您应在安装时使用 dist:fusing 清单属性添加功能模块。如需了解详情,请参阅功能模块清单
NETWORK_ERROR 由于出现网络连接错误,请求失败。 提示用户建立网络连接或更改为其他网络。
ACCESS_DENIED 由于权限不足,应用无法注册该请求。 通常,当应用在后台运行时,会出现这种情况。在应用返回到前台时尝试请求。
INCOMPATIBLE_WITH_EXISTING_SESSION 该请求包含一个或多个已请求但尚未安装的模块。 创建一个新请求,该请求不包含应用已请求的模块,或等待所有当前已请求的模块完成安装,然后再重试请求。

请注意,请求已安装的模块无法解决错误。

SERVICE_DIED 负责处理请求的服务已终止。 请重试请求。

您的 SplitInstallStateUpdatedListener 会收到 SplitInstallSessionState,包含此错误代码,状态为FAILED,会话 ID 为 -1

INSUFFICIENT_STORAGE 设备没有足够的可用存储空间,无法安装功能模块。 通知用户他们没有足够的存储空间,无法安装此功能。
SPLITCOMPAT_VERIFICATION_ERROR、SPLITCOMPAT_EMULATION_ERROR、SLIITCOMPAT_COPY_ERROR SplitCompat 无法加载功能模块。 这些错误应该会在下次应用重启后自动得到解决。
PLAY_STORE_NOT_FOUND 设备上未安装 Play 商店应用。 告知用户必须安装 Play 商店应用才能下载此功能。
APP_NOT_OWNED Google Play 尚未安装该应用,因此无法下载功能。只有在延迟安装时才会出现此问题。 如果您想让用户在 Google Play 上获取应用,请使用 startInstall(),它可以获取必要的用户确认
INTERNAL_ERROR Play 商店内发生内部错误。 请重试请求。

如果用户请求下载按需模块并出现错误,请考虑显示一个对话框并为用户提供如下两个选项:重试(再次尝试该请求)和取消(放弃该请求)。如需其他支持,您还应该提供帮助链接,引导用户访问 Google Play 帮助中心

处理状态更新

注册监听器并记录请求的会话 ID 后,请使用 StateUpdatedListener.onStateUpdate() 处理状态变更,如下所示。

Kotlin

override fun onStateUpdate(state : SplitInstallSessionState) {
    if (state.status() ==