設定隨選提供功能

功能模組可讓您將特定功能和資源與應用程式的基本模組分開,並納入應用程式套件中。舉例來說,透過 Play Feature Delivery,使用者可以先安裝您應用程式的基準 APK,再視情況下載及安裝這些元件。

例如,假設有一個簡訊應用程式含有擷取和傳送圖片訊息的功能,但只有一小部分的使用者會傳送圖片訊息。將圖片訊息當做可下載的功能模組也是合理的做法。這樣一來,所有使用者下載這個應用程式的初始時間會比較少,只有傳送圖片訊息的使用者需要下載這個額外元件。

請注意,這類模組化工作需要投注更多心力,也可能需要重構應用程式的現有程式碼,因此請仔細考慮應用程式可以隨選提供給使用者並獲益最多的功能。如要進一步瞭解隨選功能的最佳使用案例與指南,請參閱隨選提供使用者體驗的最佳做法

如果您想要逐步翻新應用程式功能,而不啟用進階提供選項 (例如隨選提供),請改為設定安裝時提供

這個頁面可協助您將功能模組新增至應用程式專案,並視需求進行設定。開始之前,請先確認您使用的是 Android Studio 3.5 或以上版本,以及 Android Gradle 外掛程式 3.5.0 或以上版本。

設定隨選提供的新模組

如要建立新功能模組,最簡單的方法是使用 Android Studio 3.5 或以上版本。由於功能模組具有基礎應用程式模組的固有依附元件,因此您只能將其新增至現有的應用程式專案。

如要使用 Android Studio 將功能模組新增至應用程式專案,請按照下列步驟操作:

  1. 如果您尚未這麼做,請在 IDE 中開啟應用程式專案。
  2. 在選單列中,依序選取「檔案」>「新增」>「新增模組」。
  3. 在「建立新模組」對話方塊中,選取「動態功能模組」,然後按一下「下一步」。
  4. 在「設定新的模組」部分中完成下列步驟:
    1. 從下拉式選單中選取應用程式專案的基礎應用程式模組
    2. 指定模組名稱。IDE 會使用這個名稱,在 Gradle 設定檔中將該模組視為 Gradle 子專案。當您建立應用程式套件時,Gradle 會使用子專案名稱中的最後一個元素,在功能模組資訊清單中插入 <manifest split> 屬性。
    3. 指定模組的套件名稱。在預設的情況下,Android Studio 會建議一個套件名稱,該名稱結合了基礎模組的根套件名稱以及您在上一步中指定的模組名稱。
    4. 選擇您希望模組支援的最低 API 級別。這個值應與基礎模組的值相符。
  5. 按一下「下一步」
  6. 在「模組下載選項」部分中,完成以下操作:

    1. 指定最多 50 個半形字元的模組標題。平台會使用這個標題來向使用者顯示該模組,例如在確認使用者是否要下載模組時。因此,應用程式的基礎模組必須包含模組標題作為字串資源,以供翻譯。使用 Android Studio 建立模組時,IDE 會將字串資源新增至基礎模組,並在功能模組的資訊清單中插入下列項目:

      <dist:module
          ...
          dist:title="@string/feature_title">
      </dist:module>
      
    2. 在「安裝時納入」下方的下拉式選單中,選取「不要在安裝時包含模組」。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. 按一下「完成」

在 Android Studio 建立好模組後,請從「專案」窗格中自行檢查內容 (從選單列中依序選取「檢視畫面」>「工具視窗」>「專案」)。預設程式碼、資源及機構組織應與其標準應用程式模組類似。

接下來,您必須透過 Play Core 程式庫導入隨選安裝功能。

將 Play Core 程式庫納入專案

開始之前,您必須先在專案中新增 Play Core 程式庫

要求隨選模組

當應用程式需要使用功能模組時,您可以透過 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 Core 程式庫會採用「射後不理」策略。也就是說,會向平台發送模組下載請求,但不會監控是否安裝成功。如要在安裝後繼續推進使用者歷程,或順利處理錯誤,請務必監控要求狀態

注意:您也可以要求裝置上已安裝的功能模組。如果 API 偵測到要求已安裝模組,就會將要求視為已完成。此外,在安裝模組後,Google Play 也會持續自動更新。也就是說,當您上傳新版本的應用程式套件時,平台會更新所有屬於您應用程式的已安裝 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() 來要求模組,如上一節所示。

監控要求狀態

如要更新進度列、在安裝後啟動意圖,或是正常處理要求錯誤,您需要監聽非同步 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.
                    }
                }
            }
        });
}

下表列出應用程式可能需要處理的錯誤狀態:

錯誤代碼 說明 [SuggestedAction]
ACTIVE_SESSIONS_LIMIT_EXCEEDED 目前已有至少一個下載中的現有要求,因此要求遭到拒絕。 查看是否仍在下載任何要求 (如上方範例所示)。
MODULE_UNAVAILABLE 根據目前安裝的應用程式、裝置和使用者的 Google Play 帳戶,Google Play 找不到指定的模組。 如果使用者無法存取模組,請通知對方。
INVALID_REQUEST Google Play 已收到要求,但要求無效。 請確認要求中的資訊是否完整且正確。
未找到工作階段 未找到指定階段 ID 的工作階段。 如要根據工作階段 ID 監控要求的狀態,請確認工作階段 ID 正確無誤。
API_NOT_AVAILABLE 目前的裝置不支援 Play Core 程式庫。換句話說,裝置無法隨選下載及安裝功能。 如果是搭載 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, SPLITCOMPAT_COPY_ERROR SplitCompat 無法載入功能模組。 這些錯誤將在系統下次重新啟動後自動解決。
PLAY_STORE_NOT_FOUND 裝置上沒有安裝 Play 商店應用程式。 讓使用者知道必須使用 Play 商店應用程式才能下載這項功能。
APP_NOT_OWNED 這個應用程式尚未從 Google Play 安裝,因此無法下載。在 Play Core 1.9 或以上版本中,只有延遲安裝會發生這個錯誤。 如果您希望使用者從 Google Play 取得應用程式,請使用 startInstall() 取得必要的使用者確認 (Play Core 1.9 或以上版本)。
INTERNAL_ERROR Play 商店發生內部錯誤。 重試要求。

如果使用者要求下載隨選模組,但發生錯誤,請考慮為使用者顯示有兩個選項的對話方塊:再試一次 (重新嘗試傳送要求) 和取消 (放棄要求)。如需其他支援,請一併提供說明連結,將使用者導向 Google Play 說明中心

處理狀態更新

註冊監聽器並記錄要求的工作階段 ID 後,請使用 StateUpdatedListener.onStateUpdate() 來處理狀態變更,如下所示。

Kotlin

override fun onStateUpdate(state : SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIED) {
       // Retry the request.
       return
    }
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.DOWNLOADING -> {
              val totalBytes = state.totalBytesToDownload()
              val progress = state.bytesDownloaded()
              // Update progress bar.
            }
            SplitInstallSessionStatus.INSTALLED -> {

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.FAILED
        && state.errorCode() == SplitInstallErrorCode.SERVICE_DIES) {
       // Retry the request.
       return;
    }
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.DOWNLOADING:
              int totalBytes = state.totalBytesToDownload();
              int progress = state.bytesDownloaded();
              // Update progress bar.
              break;

            case SplitInstallSessionStatus.INSTALLED:

              // After a module is installed, you can start accessing its content or
              // fire an intent to start an activity in the installed module.
              // For other use cases, see access code and resources from installed modules.

              // If the request is an on demand module for an Android Instant App
              // running on Android 8.0 (API level 26) or higher, you need to
              // update the app context using the SplitInstallHelper API.
        }
    }
}

下表說明安裝要求的可能狀態。

要求狀態 說明 [SuggestedAction]
待處理 系統已接受要求,很快就會開始下載。 初始化使用者介面元件 (例如進度列),以向使用者提供下載意見回饋。
REQUIRES_USER_CONFIRMATION 下載作業需要使用者確認。最常見的原因是應用程式尚未透過 Google Play 安裝。 提示使用者透過 Google Play 確認下載該功能。詳情請參閱如何獲得使用者確認一節。
下載中 正在下載 如要提供下載進度列,請使用 SplitInstallSessionState.bytesDownloaded()SplitInstallSessionState.totalBytesToDownload() 方法更新使用者介面 (請參閱上方資料表的程式碼範例)。
已下載 裝置已下載模組,但尚未開始安裝。 應用程式應啟用 SplitCompat 才能存取下載的模組,並避免看到這個狀態。您必須授予權限,才能存取功能模組的程式碼和資源。
安裝中 裝置目前正在安裝模組。 更新進度列。這個狀態通常很短。
已安裝 模組已安裝在裝置上。 存取模組中的程式碼和資源,以繼續使用者歷程。

如果模組適用於搭載 Android 8.0 (API 級別 26) 或以上版本的 Android 免安裝應用程式,則必須使用 splitInstallHelper使用新模組更新應用程式元件

失敗 要求在裝置安裝模組之前失敗。 提示使用者重試要求或取消要求。
正在取消 裝置正在取消要求。 詳情請參閱取消安裝要求一節。
已取消 要求已取消。

獲得使用者確認

在某些情況下,Google Play 可能會要求使用者必須確認才能滿足下載要求。舉例來說,您的應用程式尚未從 Google Play 安裝,或是您正嘗試透過行動數據下載大量資料。在這類情況下,要求的狀態為 REQUIRES_USER_CONFIRMATION,而您的應用程式需要獲得使用者確認,裝置才能下載並安裝要求中的模組。如要確認應用程式是否成功,應用程式應按照下列提示提示使用者:

Kotlin

override fun onSessionStateUpdate(state: SplitInstallSessionState) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          /* activity = */ this,
          // You use this request code to later retrieve the user's decision.
          /* requestCode = */ MY_REQUEST_CODE)
    }
    ...
 }

Java

@Override void onSessionStateUpdate(SplitInstallSessionState state) {
    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
        // Displays a confirmation for the user to confirm the request.
        splitInstallManager.startConfirmationDialogForResult(
          state,
          /* activity = */ this,
          // You use this request code to later retrieve the user's decision.
          /* requestCode = */ MY_REQUEST_CODE);
    }
    ...
 }

要求的狀態會根據使用者回應而更新:

  • 如果使用者接受確認,要求的狀態會變更為 PENDING,並繼續下載。
  • 如果使用者拒絕確認,要求狀態就會變更為 CANCELED
  • 如果使用者在對話方塊被刪除前未選取任何項目,則要求狀態會顯示為 REQUIRES_USER_CONFIRMATION。您的應用程式可再次提示使用者完成要求。

如要接收使用者回應的回呼,請使用 onActivityResult(),如下所示。

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  if (requestCode == MY_REQUEST_CODE) {
    // Handle the user's decision. For example, if the user selects "Cancel",
    // you may want to disable certain functionality that depends on the module.
  }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == MY_REQUEST_CODE) {
    // Handle the user's decision. For example, if the user selects "Cancel",
    // you may want to disable certain functionality that depends on the module.
  }
}

取消安裝要求

如果應用程式必須在安裝前取消要求,則可以使用要求的工作階段 ID 叫用 cancelInstall() 方法,如下所示。

Kotlin

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId)

Java

splitInstallManager
    // Cancels the request for the given session ID.
    .cancelInstall(mySessionId);

存取模組

如要從已下載的模組存取程式碼和資源,您的應用程式需要為應用程式以及應用程式下載的功能模組中的每項活動啟用 SplitCompat 程式庫

但請注意,該平台在下載模組後一段時間 (有時甚至是幾天) 會遇到以下限制:

  • 平台無法套用由模組導入的任何新資訊清單項目。
  • 平台無法存取系統使用者介面元件 (例如通知) 的資源。如果您需要立即使用這些資源,請考慮將這些資源納入應用程式的基本模組。

啟用 SplitCompat

為了讓應用程式從下載的模組中存取程式碼和資源,您必須僅使用以下各節所述的方法之一啟用 SplitCompat。

為應用程式啟用 SplitCompat 後,您必須在應用程式的模組中為每項活動啟用 SplitCompat

在資訊清單中宣告 SplitCompatApplication

啟用 SplitCompat 最簡單的方式是在應用程式資訊清單中將 SplitCompatApplication 宣告為 Application 子類別,如下所示:

<application
    ...
    android:name="com.google.android.play.core.splitcompat.SplitCompatApplication">
</application>

在裝置上安裝應用程式後,您就可以自動從已下載的功能模組存取程式碼和資源。

在執行階段叫用 SplitCompat

您也可以在執行階段的特定活動或服務中啟用 SplitCompat。如要啟動功能模組中的活動,您必須啟用 SplitCompat。方法是覆寫 attachBaseContext,如下所示。

如果您有自訂 Application 類別,請改為擴充 SplitCompatApplication,以便為應用程式啟用 SplitCompat,如下所示:

Kotlin

class MyApplication : SplitCompatApplication() {
    ...
}

Java

public class MyApplication extends SplitCompatApplication {
    ...
}

SplitCompatApplication 只會覆寫 ContextWrapper.attachBaseContext() 以加入 SplitCompat.install(Context applicationContext)。如果您不希望 Application 類別擴充 SplitCompatApplication,可以手動覆寫 attachBaseContext() 方法,如下所示:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
}

如果您的隨選模組與免安裝應用程式和已安裝的應用程式相容,請有條件叫用 SplitCompat,如下所示:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this)
    }
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    if (!InstantApps.isInstantApp(this)) {
        SplitCompat.install(this);
    }
}

啟用模組活動專用的 SplitCompat

您為基本應用程式啟用 SplitCompat 後,您必須在應用程式模組中的每項活動中啟用 SplitCompat。方法是使用 SplitCompat.installActivity() 方法,如下所示:

Kotlin

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this);
}

存取功能模組中定義的元件

啟動功能模組中定義的活動

啟用 SplitCompat 後,即可使用 startActivity() 啟動功能模組中定義的活動。

Kotlin

startActivity(Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...))

Java

startActivity(new Intent()
  .setClassName("com.package", "com.package.module.MyActivity")
  .setFlags(...));

setClassName 的第一個參數是應用程式的套件名稱,第二個參數則是活動的完整類別名稱。

在隨選下載的功能模組中發生活動時,您必須在活動中啟用 SplitCompat

啟動功能模組中定義的服務

啟用 SplitCompat 後,即可透過 startService() 啟動功能模組中定義的服務。

Kotlin

startService(Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...))

Java

startService(new Intent()
  .setClassName("com.package", "com.package.module.MyService")
  .setFlags(...));

匯出功能模組中定義的元件

匯出的 Android 元件不應納入選用模組。

建構系統會將所有模組的資訊清單項目合併到基本模組中;如果選用的模組包含匯出的元件,在安裝模組之前仍可存取,但可能因為缺少程式碼而導致當機。

這不是內部元件的問題,應用程式只能存取應用程式,因此應用程式可在存取元件前檢查模組是否已安裝

如果您需要匯出的元件,並希望內容出現在選用模組中,請考慮實作 Proxy 模式。方法是在基礎中加入 Proxy 匯出元件,存取時,Proxy 元件就能檢查內含該內容的模組是否存在。如果有該模組,Proxy 元件可以透過 Intent 從模組啟動內部元件,從呼叫端應用程式轉發意圖。如果模組不存在,元件可以下載模組,或將適當的錯誤訊息傳回呼叫應用程式。

從已安裝的模組存取程式碼和資源

如果您已為基本應用程式結構定義和功能模組中的活動啟用 SplitCompat,若已安裝可選模組,您便可以像使用基本 APK 的一部分一樣使用功能模組中的程式碼和資源。

透過其他模組存取程式碼

透過模組存取基本程式碼

基本模組中的程式碼可直接由其他模組使用。您不需要採取任何其他行動;只要匯入並使用您需要的類別即可。

透過其他模組存取模組程式碼

模組中的物件或類別無法直接從其他模組靜態存取,但可以使用反射方式間接存取。

由於反射產生的效能成本,您應留意這種情況發生的頻率。如果是複雜的用途,請使用 Dagger 2 等依附元件插入架構,確保每個應用程式生命週期內僅有一次反射呼叫。

為了簡化執行個體化後與物件的互動情形,建議您在基本模組中定義介面及其在功能模組中的實作項目。例如:

Kotlin

// In the base module
interface MyInterface {
  fun hello(): String
}

// In the feature module
object MyInterfaceImpl : MyInterface {
  override fun hello() = "Hello"
}

// In the base module, where we want to access the feature module code
val stringFromModule = (Class.forName("com.package.module.MyInterfaceImpl")
    .kotlin.objectInstance as MyInterface).hello();

Java

// In the base module
public interface MyInterface {
  String hello();
}

// In the feature module
public class MyInterfaceImpl implements MyInterface {
  @Override
  public String hello() {
    return "Hello";
  }
}

// In the base module, where we want to access the feature module code
String stringFromModule =
   ((MyInterface) Class.forName("com.package.module.MyInterfaceImpl").getConstructor().newInstance()).hello();

透過其他模組存取資源和資產

安裝模組後,您可以透過標準方式存取模組中的資源和資產,並注意以下兩點:

  • 如果您是從其他模組存取資源,該模組將無法存取資源 ID,但您仍可透過名稱存取資源。請注意,用於參照資源的套件是定義資源的模組套件。
  • 如果想從應用程式的不同已安裝模組存取新安裝模組中存在的資產或資源,您必須使用應用程式結構進行。嘗試存取資源的元件結構尚未更新。您也可以在功能模組安裝完成後,重新建立該元件 (例如呼叫 Activity.recreate()) 或重新安裝 SplitCompat

從選用模組載入原生程式碼

安裝分割區後,叫用標準 System.loadLibrary(libName) 即可載入其原生程式碼。針對免安裝應用程式,我們提供了特殊方法

如果您使用 System.loadLibrary() 載入原生程式碼,且您的原生程式庫對模組中的另一個程式庫有依附元件,您必須先手動載入該程式庫。

如果您在原生程式碼中使用 dlopen() 載入以選用模組定義的程式庫,該程式庫無法與相對程式庫路徑搭配使用。最理想的解決方案是透過 ClassLoader.findLibrary() 從 Java 程式碼擷取程式庫的絕對路徑,然後在 dlopen() 呼叫中使用該程式庫。請先執行這項操作,再輸入原生程式碼,或使用原生程式碼的 JNI 呼叫至 Java。

存取已安裝的 Android 免安裝應用程式

在 Android 免安裝應用程式模組回報為 INSTALLED 後,您可以使用重新整理的應用程式結構定義存取其程式碼和資源。您的應用程式在安裝模組之前建立的結構定義 (例如已儲存在變數中) 並不包含新模組的內容。但一個新的背景資訊可以,例如使用 createPackageContext 即可獲得。

Kotlin

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                val newContext = context.createPackageContext(context.packageName, 0)
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                val am = newContext.assets
            }
        }
    }
}

Java

// Generate a new context as soon as a request for a new module
// reports as INSTALLED.
@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // If you use AssetManager to access your app’s raw asset files, you’ll need
                // to generate a new AssetManager instance from the updated context.
                AssetManager am = newContext.getAssets();
        }
    }
}

Android 8.0 及以上版本的 Android 免安裝應用程式

在 Android 8.0 (API 級別 26) 及以上版本的 Android 免安裝應用程式中,以隨選方式要求模組,您需在執行 INSTALLED 安裝要求報表後,根據新的模組以呼叫 SplitInstallHelper.updateAppInfo(Context context)。否則應用程式就無法得知模組的程式碼和資源。更新應用程式的中繼資料後,請叫用新的 Handler,在下一個主要執行緒事件中載入模組內容,如下所示:

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState ) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            ...
            SplitInstallSessionStatus.INSTALLED -> {
                // You need to perform the following only for Android Instant Apps
                // running on Android 8.0 (API level 26) and higher.
                if (BuildCompat.isAtLeastO()) {
                    // Updates the app’s context with the code and resources of the
                    // installed module.
                    SplitInstallHelper.updateAppInfo(context)
                    Handler().post {
                        // Loads contents from the module using AssetManager
                        val am = context.assets
                        ...
                    }
                }
            }
        }
    }
}

Java

@Override
public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            ...
            case SplitInstallSessionStatus.INSTALLED:
            // You need to perform the following only for Android Instant Apps
            // running on Android 8.0 (API level 26) and higher.
            if (BuildCompat.isAtLeastO()) {
                // Updates the app’s context with the code and resources of the
                // installed module.
                SplitInstallHelper.updateAppInfo(context);
                new Handler().post(new Runnable() {
                    @Override public void run() {
                        // Loads contents from the module using AssetManager
                        AssetManager am = context.getAssets();
                        ...
                    }
                });
            }
        }
    }
}

載入 C/C++ 程式庫

如要從已從免安裝應用程式下載的模組載入 C/C++ 程式庫,請使用 SplitInstallHelper.loadLibrary(Context context, String libName),如下所示:

Kotlin

override fun onStateUpdate(state: SplitInstallSessionState) {
    if (state.sessionId() == mySessionId) {
        when (state.status()) {
            SplitInstallSessionStatus.INSTALLED -> {
                // Updates the app’s context as soon as a module is installed.
                val newContext = context.createPackageContext(context.packageName, 0)
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”)
                ...
            }
        }
    }
}

Java

public void onStateUpdate(SplitInstallSessionState state) {
    if (state.sessionId() == mySessionId) {
        switch (state.status()) {
            case SplitInstallSessionStatus.INSTALLED:
                // Updates the app’s context as soon as a module is installed.
                Context newContext = context.createPackageContext(context.getPackageName(), 0);
                // To load C/C++ libraries from an installed module, use the following API
                // instead of System.load().
                SplitInstallHelper.loadLibrary(newContext, “my-cpp-lib”);
                ...
        }
    }
}

已知限制

  • 如果 Android WebView 是透過選用模組存取資源或資產,您就無法使用這個活動。這是因為 Android API 級別 28 及以下版本的 WebView 和 SplitCompat 不相容。
  • 您無法快取 Android ApplicationInfo 物件、其內容或在應用程式中包含其的物件。建議您隨時從應用程式結構中擷取這些物件。快取這些物件可能會導致應用程式在安裝功能模組時異常終止。

管理已安裝的模組

如要查看裝置目前已安裝了哪些功能模組,您可以呼叫 SplitInstallManager.getInstalledModules(),傳回 Set<String> 的已安裝模組名稱,如下所示。

Kotlin

val installedModules: Set<String> = splitInstallManager.installedModules

Java

Set<String> installedModules = splitInstallManager.getInstalledModules();

解除安裝模組

您可以叫用 SplitInstallManager.deferredUninstall(List<String> moduleNames) 來要求裝置解除安裝模組,如下所示。

Kotlin

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(listOf("pictureMessages", "promotionalFilters"))

Java

// Specifies two feature modules for deferred uninstall.
splitInstallManager.deferredUninstall(Arrays.asList("pictureMessages", "promotionalFilters"));

系統不會立即解除安裝模組。也就是說,裝置會視需要在背景解除安裝這些應用程式,以節省儲存空間。您可以叫用 SplitInstallManager.getInstalledModules() 並檢查前一節所述的結果,確認裝置已刪除模組。

下載其他語言資源

使用應用程式套件時,裝置只會下載執行應用程式所需的程式碼和資源。因此,對於語言資源,使用者的裝置只會依裝置設定中當下選取的一或多種語言,下載相符的應用程式語言資源。

如果您希望應用程式能存取其他語言資源 (例如導入應用程式內語言挑選器),可以使用 Play Core 程式庫隨選下載。與下載功能模組的程序類似,如下所示。

Kotlin

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply()
...

// Creates a request to download and install additional language resources.
val request = SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build()

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request)

Java

// Captures the user’s preferred language and persists it
// through the app’s SharedPreferences.
sharedPrefs.edit().putString(LANGUAGE_SELECTION, "fr").apply();
...

// Creates a request to download and install additional language resources.
SplitInstallRequest request =
    SplitInstallRequest.newBuilder()
        // Uses the addLanguage() method to include French language resources in the request.
        // Note that country codes are ignored. That is, if your app
        // includes resources for “fr-FR” and “fr-CA”, resources for both
        // country codes are downloaded when requesting resources for "fr".
        .addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
        .build();

// Submits the request to install the additional language resources.
splitInstallManager.startInstall(request);

系統會將該要求視為功能模組的要求。也就是說,您可以照常監控要求狀態

如果您的應用程式不需要立即使用其他語言資源,當應用程式在背景時,可以延遲安裝作業,如下所示。

Kotlin

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageInstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

存取已下載的語言資源

如要存取下載的語言資源,您的應用程式必須在每個需要存取這些資源的活動的 attachBaseContext() 方法中執行 SplitCompat.installActivity() 方法,如下所示。

Kotlin

override fun attachBaseContext(base: Context) {
  super.attachBaseContext(base)
  SplitCompat.installActivity(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  super.attachBaseContext(base);
  SplitCompat.installActivity(this);
}

針對您要使用的應用程式資源的每個活動,更新基本結構定義,然後透過其 Configuration 設定新的程式碼:

Kotlin

override fun attachBaseContext(base: Context) {
  val configuration = Configuration()
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))
  val context = base.createConfigurationContext(configuration)
  super.attachBaseContext(context)
  SplitCompat.install(this)
}

Java

@Override
protected void attachBaseContext(Context base) {
  Configuration configuration = new Configuration();
  configuration.setLocale(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));
  Context context = base.createConfigurationContext(configuration);
  super.attachBaseContext(context);
  SplitCompat.install(this);
}

為了讓這些變更生效,您必須在新語言安裝完成並準備使用後,重新建立活動。您可以使用 Activity#recreate() 方法。

Kotlin

when (state.status()) {
  SplitInstallSessionStatus.INSTALLED -> {
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate()
  }
  ...
}

Java

switch (state.status()) {
  case SplitInstallSessionStatus.INSTALLED:
      // Recreates the activity to load resources for the new language
      // preference.
      activity.recreate();
  ...
}

解除安裝其他語言資源

與功能模組類似,您隨時可以解除安裝其他資源。提出解除安裝要求之前,建議您先依照下列步驟判斷目前安裝的語言為何。

Kotlin

val installedLanguages: Set<String> = splitInstallManager.installedLanguages

Java

Set<String> installedLanguages = splitInstallManager.getInstalledLanguages();

接下來,您可以使用 deferredLanguageUninstall() 方法決定要解除安裝的語言,如下所示。

Kotlin

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)))

Java

splitInstallManager.deferredLanguageUninstall(
    Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION)));

本機測試模組安裝

Play Core 程式庫可以讓您在本機測試應用程式執行以下操作的能力,不必連結至 Play 商店:

本頁說明如何將應用程式的分割 APK 部署到測試裝置,讓 Play Core 自動使用這些 APK 模擬來自 Play 商店的要求、下載和安裝模組。

雖然您不需要對應用程式邏輯進行任何變更,但必須符合以下規定:

建立一組 APK

如果您尚未這樣做,請依下列步驟操作建立應用程式的分割 APK,步驟如下:

  1. 透過下列其中一種方法為您的應用程式建構應用程式套件:
  2. 使用 bundletool 即可使用下列指令為裝置設定產生一組 APK

    bundletool build-apks --local-testing
      --bundle my_app.aab
      --output my_app.apks
    

--local-testing 旗標在 APK 資訊清單中包含中繼資料,可以讓 Play Core 程式庫使用本機分割 APK 來測試安裝功能模組 (無須連結至 Play 商店)。

將您的應用程式部署至裝置

使用 --local-testing 旗標建立一組 APK 後,請使用 bundletool 安裝應用程式的基本版本,並將其他 APK 轉移到裝置本機儲存空間中。您可以使用下列指令執行這兩個動作:

bundletool install-apks --apks my_app.apks

現在,當您啟動應用程式並完成使用者流程以下載及安裝功能模組時,Play Core 程式庫會使用 bundletool 轉移到裝置本機儲存空間的 APK。

模擬網路錯誤

如要模擬從 Play 商店安裝的模組,Play Core 程式庫會使用 SplitInstallManager 的替代憑證 (稱為 FakeSplitInstallManager) 來要求模組。使用含有 --local-testing 旗標的 bundletool建立一組 APK 並部署到測試裝置時,該檔案會包含指示 Play Core 程式庫的中繼資料自動切換應用程式 API 呼叫,以叫用 FakeSplitInstallManager,而非 SplitInstallManager

FakeSplitInstallManager 包含布林值標記,可讓您在應用程式下次要求安裝模組時模擬網路錯誤。如要在測試中存取 FakeSplitInstallManager,可以使用 FakeSplitInstallManagerFactory 取得該執行個體的例項,如下所示:

Kotlin

// Creates an instance of FakeSplitInstallManager with the app's context.
val fakeSplitInstallManager = FakeSplitInstallManagerFactory.create(context)
// Tells Play Core Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true)

Java

// Creates an instance of FakeSplitInstallManager with the app's context.
FakeSplitInstallManager fakeSplitInstallManager =
    FakeSplitInstallManagerFactory.create(context);
// Tells Play Core Library to force the next module request to
// result in a network error.
fakeSplitInstallManager.setShouldNetworkError(true);