使用 Play 帳款服務程式庫第 5 版在應用程式中銷售訂閱項目

1. 簡介

Google Play 帳單系統是一項服務,可讓您在自己的 Android 應用程式中銷售數位產品和內容。為了透過應用程式營利,這是銷售應用程式內產品最直接的方式。本程式碼研究室將說明如何使用 Google Play 帳款服務程式庫在專案中販售訂閱項目,以及如何封裝購買交易與其他應用程式的其他內容。

並介紹訂閱相關概念,例如基本方案、優惠、標記和預付方案。如要進一步瞭解 Google Play 帳款服務的訂閱項目,請造訪說明中心

建構項目

在本程式碼研究室中,您會將最新的帳款服務程式庫 (5.0.0 版) 新增至以訂閱為基礎的簡易設定檔應用程式。應用程式已為您建構,您只需新增帳單部分即可。如圖 1 所示,使用者在此應用程式中註冊任何基本方案和/或優惠,包括兩種可再生能源 (基本版和進階版) 或不可續訂的預付方案。就這樣,基本方案分別為按月和按年訂閱方案。使用者可以升級、降級或將預付訂閱方案轉換為可再生能源。

d7dba51f800a6cc4.png 2220c15b849d2ead.png

如要將 Google Play 帳款服務程式庫加入應用程式,您必須建立下列項目:

  • BillingClientWrapper:BillingClient 程式庫的包裝函式。用途是用來封裝與 Play 帳款服務程式庫的 BillingClient 的互動,但自己的整合項目不需要進行這項操作。
  • SubscriptionDataRepository - 應用程式的結帳存放區,其中包含應用程式的訂閱產品目錄清單 (例如待售產品),以及可協助收集購買狀態和產品詳細資料的 ShareFlow 變數清單
  • MainViewModel - 這個 ViewModel 可讓應用程式的其餘部分與帳單存放區通訊。這有助於使用多種購買方式在 UI 中啟動結帳流程。

完成後,應用程式的架構應如下圖所示:

c83bc759f32b0a63.png

課程內容

  • 如何整合 Play 帳款服務程式庫
  • 如何透過 Play 管理中心建立訂閱產品、基本方案、優惠和標記
  • 如何從應用程式擷取可用的基本方案和優惠
  • 如何使用合適的參數啟動帳單流程
  • 如何提供預付訂閱產品

本程式碼研究室著重於 Google Play 帳款服務。我們不會對與本主題無關的概念和程式碼多做介紹,但會事先準備好這些程式碼區塊,屆時您只要複製及貼上即可。

軟硬體需求

  • 最新版 Android Studio (>= Arctic Fox | 2020.3.1)
  • 搭載 Android 8.0 以上版本的 Android 裝置
  • GitHub 為您準備的程式碼範例 (操作說明請見後續章節)
  • 對 Android Studio 進行 Android 開發作業的中級知識
  • 如何在 Google Play 商店中發布應用程式
  • 具備一般編寫 Kotlin 程式碼的經驗
  • Google Play 帳款服務程式庫 5.0.0 版

2. 開始設定

從 GitHub 取得程式碼

我們已將本專案所需的資料都放到 Git 存放區中。如要開始使用,請擷取程式碼,然後在慣用的開發環境中開啟。在這個程式碼研究室中,建議您使用 Android Studio。

開始使用的程式碼會儲存在 GitHub 存放區中。您可以透過下列指令複製存放區:

git clone https://github.com/android/play-billing-samples.git
cd PlayBillingCodelab

3. 基本作業

從哪裡開始?

我們要從專為本程式碼研究室所設計的基本使用者個人資料應用程式開始著手。程式碼已經過簡化,以便展示我們希望說明的概念,且並非專為實際工作環境設計。如果您選擇在正式版應用程式中重複使用此程式碼的任何部分,請務必遵循最佳做法,並完整測試所有程式碼。

將專案匯入 Android Studio

PlayBillingCodelab 是不含 Google Play 帳款服務實作的基本應用程式,啟動 Android Studio 並匯入帳單程式碼研究室,選擇 Open > billing/PlayBillingCodelab

這項專案有兩個模組:

  • start 具有基本架構應用程式,但缺少必要的依附元件,以及您需要實作的所有方法。
  • finished 是已完成的專案,當您遇到困難時,Google 可以提供指導。

這個應用程式包含八個類別檔案:BillingClientWrapperSubscriptionDataRepositoryComposablesMainStateMainViewModelMainViewModelFactoryMainActivity

  • BillingClientWrapper 是包裝函式,可隔離需要簡單實作的 Google Play 帳款服務 [BillingClient] 方法,並將回應發送至資料存放區進行處理。
  • SubscriptionDataRepository 是用來擷取 Google Play 帳款服務資料來源 (亦即帳款服務用戶端程式庫),並將 BillingClientWrapper 中產生的 StateFlow 資料轉換為資料流。
  • ButtonModel 是一種資料類別,用於在 UI 中建構按鈕。
  • 可組合函式會將所有 UI 的可組合方法擷取至單一類別。
  • MainState 是狀態管理的資料類別,
  • MainViewModel 是用來保存 UI 中使用的帳單相關資料和狀態。這會將 SubscriptionDataRepository 中的所有流程合併成一個狀態物件。
  • MainActivity 是主要活動類別,用於載入使用者介面的可組合函式。
  • 常數是指包含多個類別所用常數的物件。

Gradle

您必須新增 Gradle 依附元件,才能將 Google Play 帳款服務新增至應用程式。開啟應用程式模組的 build.gradle 檔案,然後新增下列內容:

dependencies {
    val billing_version = "5.0.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Google Play 管理中心

就本程式碼研究室而言,您必須在 Google Play 管理中心的「訂閱」專區建立下列兩項訂閱產品:

  • 1 個基本訂閱項目,產品 ID 為 up_basic_sub

產品應具備 3 個含相關標記的基本方案 (2 個自動續訂,以及 1 個預付制):1 個每月基本訂閱方案 (使用標記 monthlybasic)、具有 yearlybasic 標記的 1 年基本訂閱方案,以及 1 個含有 prepaidbasic 標記的基本訂閱方案

您可以在基本方案中新增優惠。優惠會沿用相關基本方案的標記。

  • 1 個付費訂閱項目 (產品 ID:up_premium_sub)

產品應具備 3 個含相關標記的基本方案 (2 個自動續訂,以及 1 個預付制):1 個每月基本訂閱方案 (使用標記 monthlypremium)、具有 yearlypremium 標記的 1 年基本訂閱方案,以及 1 個含有 prepaidpremium 標記的基本訂閱方案

a9f6fd6e70e69fed.png

您可以在基本方案中新增優惠。優惠會沿用相關基本方案的標記。

如要進一步瞭解如何建立訂閱產品、基本方案、優惠和標記,請造訪 Google Play 說明中心

4. 帳單用戶端設定完成

在本節中,您將使用 BillingClientWrapper 類別。

最後,您需要為帳單用戶端執行例項化所需的一切,以及所有相關方法。

  1. 初始化 BillingClient

加入 Google Play 帳款服務程式庫的依附元件後,需要初始化 BillingClient 例項。

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. 與 Google Play 建立連線

建立 BillingClient 後,我們需要與 Google Play 建立連線。

如要連線至 Google Play,請呼叫 startConnection()。連線程序並非同步進行,必須實作 BillingClientStateListener,才能在用戶端完成設定並準備好提出進一步要求後接收回呼。

BillingClientWrapper.kt

fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {

   billingClient.startConnection(object : BillingClientStateListener {
       override fun onBillingSetupFinished(billingResult: BillingResult) {
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               Log.d(TAG, "Billing response OK")
               // The BillingClient is ready. You can query purchases and product details here
               queryPurchases()
               queryProductDetails()
               billingConnectionState.postValue(true)
           } else {
               Log.e(TAG, billingResult.debugMessage)
           }
       }

       override fun onBillingServiceDisconnected() {
           Log.i(TAG, "Billing connection disconnected")
           startBillingConnection(billingConnectionState)
       }
   })
}
  1. 查詢 Google Play 帳款服務,瞭解現有購買交易

與 Google Play 建立連線後,我們就可以開始查詢使用者先前透過呼叫 queryPurchasesAsync() 完成的購買交易。

BillingClientWrapper.kt

fun queryPurchases() {
   if (!billingClient.isReady) {
       Log.e(TAG, "queryPurchases: BillingClient is not ready")
   }
   // Query for existing subscription products that have been purchased.
   billingClient.queryPurchasesAsync(
       QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
   ) { billingResult, purchaseList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           if (!purchaseList.isNullOrEmpty()) {
               _purchases.value = purchaseList
           } else {
               _purchases.value = emptyList()
           }

       } else {
           Log.e(TAG, billingResult.debugMessage)
       }
   }
}
  1. 顯示可供購買的產品

接著,我們就能查詢可用的產品並向使用者顯示。如要在 Google Play 查詢訂閱產品詳細資料,我們會呼叫 queryProductDetailsAsync()。向使用者顯示產品之前,查詢產品詳細資料是非常重要的步驟,因為這會傳回本地化的產品資訊。

BillingClientWrapper.kt

fun queryProductDetails() {
   val params = QueryProductDetailsParams.newBuilder()
   val productList = mutableListOf<QueryProductDetailsParams.Product>()
   for (product in LIST_OF_PRODUCTS) {

       productList.add(
           QueryProductDetailsParams.Product.newBuilder()
               .setProductId(product)
               .setProductType(BillingClient.ProductType.SUBS)
               .build()
       )

       params.setProductList(productList).let { productDetailsParams ->
           Log.i(TAG, "queryProductDetailsAsync")
           billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
       }
   }
}
  1. 設定 ProductDetails 查詢的監聽器

注意:這個方法會將對應中的查詢結果發送至 _productWithProductDetails

另請注意,查詢應傳回 ProductDetails。如果沒有這種情況,可能是因為您在 Play 管理中心設定的產品尚未啟用,或是您尚未在任何測試群組中發布採用帳單用戶端依附元件的版本。

BillingClientWrapper.kt

override fun onProductDetailsResponse(
   billingResult: BillingResult,
   productDetailsList: MutableList<ProductDetails>
) {
   val responseCode = billingResult.responseCode
   val debugMessage = billingResult.debugMessage
   when (responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           var newMap = emptyMap<String, ProductDetails>()
           if (productDetailsList.isNullOrEmpty()) {
               Log.e(
                   TAG,
                   "onProductDetailsResponse: " +
                           "Found null or empty ProductDetails. " +
                           "Check to see if the Products you requested are correctly " +
                           "published in the Google Play Console."
               )
           } else {
               newMap = productDetailsList.associateBy {
                   it.productId
               }
           }
           _productWithProductDetails.value = newMap
       }
       else -> {
           Log.i(TAG, "onProductDetailsResponse: $responseCode $debugMessage")
       }
   }
}
  1. 啟動購買流程

launchBillingFlow 是使用者點選商品購買時呼叫的方法。就會提示 Google Play 透過產品的 ProductDetails 啟動購買流程。

BillingClientWrapper.kt

fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
   if (!billingClient.isReady) {
       Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
   }
   billingClient.launchBillingFlow(activity, params)

}
  1. 設定購買作業結果的事件監聽器

使用者離開 Google Play 購買畫面時 (輕觸「購買」按鈕完成購買,或輕觸「返回」按鈕取消購買),onPurchaseUpdated() 回呼會將購買流程的結果傳回應用程式。依據 BillingResult.responseCode,您就能判斷使用者是否成功購買產品。如果 responseCode == OK,表示交易已成功完成。

onPurchaseUpdated() 回傳 Purchase 物件清單,其中包含使用者透過應用程式進行的所有購買交易。在許多其他欄位中,每個 Purchase 物件都含有產品 ID、purchaseToken 和 isAcknowledged 屬性。透過這些欄位,您就可以判斷每個 Purchase 物件是需要處理的新交易,還是不需要進一步處理的現有購買交易。

購買訂閱項目的交易與確認新購買交易的方法類似,

BillingClientWrapper.kt

override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchases: List<Purchase>?
) {
   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK
       && !purchases.isNullOrEmpty()
   ) {
       // Post new purchase List to _purchases
       _purchases.value = purchases

       // Then, handle the purchases
       for (purchase in purchases) {
           acknowledgePurchases(purchase)
       }
   } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
       Log.e(TAG, "User has cancelled")
   } else {
       // Handle any other error codes.
   }
}
  1. 處理購買交易 (驗證和確認購買交易)

使用者完成購買後,應用程式需要透過確認的方式處理購買交易。

此外,成功處理確認時,_isNewPurchaseAcknowledged 的值會設為 true。

BillingClientWrapper.kt

private fun acknowledgePurchases(purchase: Purchase?) {
   purchase?.let {
       if (!it.isAcknowledged) {
           val params = AcknowledgePurchaseParams.newBuilder()
               .setPurchaseToken(it.purchaseToken)
               .build()

           billingClient.acknowledgePurchase(
               params
           ) { billingResult ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
                   it.purchaseState == Purchase.PurchaseState.PURCHASED
               ) {
                   _isNewPurchaseAcknowledged.value = true
               }
           }
       }
   }
}
  1. 終止帳單連結

最後,在刪除活動時,您想要終止與 Google Play 的連線,因此呼叫 endConnection() 即可。

BillingClientWrapper.kt

fun terminateBillingConnection() {
   Log.i(TAG, "Terminating connection")
   billingClient.endConnection()
}

5. SubscriptionDataRepository

BillingClientWrapper 中,QueryPurchasesAsyncQueryProductDetails 的回應會分別發布至 MutableStateFlow _purchases_productWithProductDetails,這些回應會在類別外以購買交易和 productWithProductDetails 公開。

SubscriptionDataRepository 中,系統會根據退貨購買交易的產品 (hasRenewableBasichasPrepaidBasichasRenewablePremiumhasPremiumPrepaid),將購買交易處理成三種流程。

此外,productWithProductDetails 會被處理到各自的 basicProductDetailspremiumProductDetails 資料流中。

6. MainViewModel

困難部分就完成了。現在,您要定義 MainViewModel,這是做為用戶端的公開介面,因此他們不必瞭解 BillingClientWrapperSubscriptionDataRepository 的內部。

首先在 MainViewModel 中,我們會在 viewModel 初始化時啟動計費連線。

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

然後,存放區中的資料流會分別合併為 productsForSaleFlows (適用於可用產品) 和 userCurrentSubscriptionFlow (針對使用者當前和有效訂閱項目),如存放區類別中處理。

目前購買交易清單也會透過 currentPurchasesFlow 顯示在使用者介面中。

MainViewModel.kt

val productsForSaleFlows = combine(
   repo.basicProductDetails,
   repo.premiumProductDetails
) { basicProductDetails,
   premiumProductDetails
   ->
   MainState(
       basicProductDetails = basicProductDetails,
       premiumProductDetails = premiumProductDetails
   )
}

// The userCurrentSubscriptionFlow object combines all the possible subscription flows into one
// for emission.
private val userCurrentSubscriptionFlow = combine(
   repo.hasRenewableBasic,
   repo.hasPrepaidBasic,
   repo.hasRenewablePremium,
   repo.hasPrepaidPremium
) { hasRenewableBasic,
   hasPrepaidBasic,
   hasRenewablePremium,
   hasPrepaidPremium
   ->
   MainState(
       hasRenewableBasic = hasRenewableBasic,
       hasPrepaidBasic = hasPrepaidBasic,
       hasRenewablePremium = hasRenewablePremium,
       hasPrepaidPremium = hasPrepaidPremium
   )
}

// Current purchases.
val currentPurchasesFlow = repo.purchases

合併的 userCurrentSubscriptionFlow 會在 init 區塊收集,並將值發布至名為 _destinationScreen 的 MutableLiveData 物件。

init {
   viewModelScope.launch {
       userCurrentSubscriptionFlow.collectLatest { collectedSubscriptions ->
           when {
               collectedSubscriptions.hasRenewableBasic == true &&
                       collectedSubscriptions.hasRenewablePremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasRenewablePremium == true &&
                       collectedSubscriptions.hasRenewableBasic == false -> {
                   _destinationScreen.postValue(DestinationScreen.PREMIUM_RENEWABLE_PROFILE)
               }
               collectedSubscriptions.hasPrepaidBasic == true &&
                       collectedSubscriptions.hasPrepaidPremium == false -> {
                   _destinationScreen.postValue(DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN)
               }
               collectedSubscriptions.hasPrepaidPremium == true &&
                       collectedSubscriptions.hasPrepaidBasic == false -> {
                   _destinationScreen.postValue(
                       DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN
                   )
               }
               else -> {
                   _destinationScreen.postValue(DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN)
               }
           }
       }

   }
}

MainViewModel 也新增了一些非常實用的方法:

  1. 擷取基本方案和優惠權杖

從 Play 帳款服務程式庫 5.0.0 版開始,所有訂閱產品都可以有多個基本方案和優惠,但不含優惠的預付基本方案除外。

這個方法可透過新推出的代碼概念,將相關優惠分組,協助使用者擷取所有符合資格的優惠和基本方案。

舉例來說,如果使用者嘗試購買基本月費訂閱方案,與每月基本訂閱產品相關聯的所有基本方案和優惠都會加上 monthlyBasic 字串標記。

MainViewModel.kt

private fun retrieveEligibleOffers(
   offerDetails: MutableList<ProductDetails.SubscriptionOfferDetails>,
   tag: String
): List<ProductDetails.SubscriptionOfferDetails> {
   val eligibleOffers = emptyList<ProductDetails.SubscriptionOfferDetails>().toMutableList()
   offerDetails.forEach { offerDetail ->
       if (offerDetail.offerTags.contains(tag)) {
           eligibleOffers.add(offerDetail)
       }
   }

   return eligibleOffers
}
  1. 最低價格計算

當使用者符合多項優惠的資格時,系統會使用 leastPricedOfferToken() 方法,計算出 retrieveEligibleOffers() 傳回的最低優惠。

這個方法會傳回所選優惠的優惠 ID 權杖。

這種實作方式只會根據 pricingPhases 組合傳回最低價商品,且不計入平均值。

另一種導入方式,則是改為根據平均價格最低的產品/服務出價。

MainViewModel.kt

private fun leastPricedOfferToken(
   offerDetails: List<ProductDetails.SubscriptionOfferDetails>
): String {
   var offerToken = String()
   var leastPricedOffer: ProductDetails.SubscriptionOfferDetails
   var lowestPrice = Int.MAX_VALUE

   if (!offerDetails.isNullOrEmpty()) {
       for (offer in offerDetails) {
           for (price in offer.pricingPhases.pricingPhaseList) {
               if (price.priceAmountMicros < lowestPrice) {
                   lowestPrice = price.priceAmountMicros.toInt()
                   leastPricedOffer = offer
                   offerToken = leastPricedOffer.offerToken
               }
           }
       }
   }
   return offerToken
}
  1. BillingFlowParams 建構工具

如要啟動特定產品的購買流程,必須設定 ProductDetails 和所選優惠的權杖,並用來建構 BilingFlowParams。

你可以透過以下兩種方式解決這個問題:

upDowngradeBillingFlowParamsBuilder() 會建構升級和降級的參數。

MainViewModel.kt

private fun upDowngradeBillingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String,
   oldToken: String
): BillingFlowParams {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   ).setSubscriptionUpdateParams(
       BillingFlowParams.SubscriptionUpdateParams.newBuilder()
           .setOldPurchaseToken(oldToken)
           .setReplaceProrationMode(
               BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE
           )
           .build()
   ).build()
}

billingFlowParamsBuilder() 會建構一般購買交易的參數。

MainViewModel.kt

private fun billingFlowParamsBuilder(
   productDetails: ProductDetails,
   offerToken: String
): BillingFlowParams.Builder {
   return BillingFlowParams.newBuilder().setProductDetailsParamsList(
       listOf(
           BillingFlowParams.ProductDetailsParams.newBuilder()
               .setProductDetails(productDetails)
               .setOfferToken(offerToken)
               .build()
       )
   )
}
  1. 購買方式

購買方式會使用 BillingClientWrapperlaunchBillingFlow()BillingFlowParams 啟動購買交易。

MainViewModel.kt

fun buy(
   productDetails: ProductDetails,
   currentPurchases: List<Purchase>?,
   activity: Activity,
   tag: String
) {
   val offers =
       productDetails.subscriptionOfferDetails?.let {
           retrieveEligibleOffers(
               offerDetails = it,
               tag = tag.lowercase()
           )
       }
   val offerToken = offers?.let { leastPricedOfferToken(it) }
   val oldPurchaseToken: String

   // Get current purchase. In this app, a user can only have one current purchase at
   // any given time.
   if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size == MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // This either an upgrade, downgrade, or conversion purchase.
       val currentPurchase = currentPurchases.first()

       // Get the token from current purchase.
       oldPurchaseToken = currentPurchase.purchaseToken

       val billingParams = offerToken?.let {
           upDowngradeBillingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it,
               oldToken = oldPurchaseToken
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams
           )
       }
   } else if (currentPurchases == null) {
       // This is a normal purchase.
       val billingParams = offerToken?.let {
           billingFlowParamsBuilder(
               productDetails = productDetails,
               offerToken = it
           )
       }

       if (billingParams != null) {
           billingClient.launchBillingFlow(
               activity,
               billingParams.build()
           )
       }
   } else if (!currentPurchases.isNullOrEmpty() &&
       currentPurchases.size > MAX_CURRENT_PURCHASES_ALLOWED
   ) {
       // The developer has allowed users  to have more than 1 purchase, so they need to
       /// implement a logic to find which one to use.
       Log.d(TAG, "User has more than 1 current purchase.")
   }
}

d726a27add092140.png

  1. 終止帳單連線

最後,系統會在 ViewModel 的 onCleared() 上呼叫 BillingClientWrapperterminateBillingConnection 方法。

這會在相關聯的活動刪除時終止目前的帳單連結。

7. 使用者介面

現在可以使用您在 UI 中建立的所有內容。為協助您完成這項作業,建議您使用 Composables 和 MainActivity 類別。

Composables.kt

Composables 類別全面提供,並定義用於轉譯 UI 的所有 Compose 函式,以及介面之間的導覽機制。

Subscriptions 函式會顯示兩個按鈕:Basic SubscriptionPremium Subscription

Basic SubscriptionPremium Subscription 會分別載入新的 Compose 方法,顯示三種基本方案:每月、每年和預付。

接著,使用者可能擁有的特定訂閱項目會有三種不同的設定檔組合:可再生能源、可再生能源,以及基本型或預付型進階版付款資料。

  • 使用者擁有基本版訂閱項目時,基本設定檔可讓使用者升級至按月、按年或預付的付費訂閱方案。
  • 相反地,如果使用者採用付費訂閱方案,他們也可以降級為按月、按年或預付基本訂閱方案。
  • 使用者採用預付型訂閱方案時,可以使用「向上」按鈕為訂閱項目儲值,或將預付型訂閱項目轉換為相應的自動續約型基本方案。

最後,載入畫面函式會用於連線至 Google Play 及轉譯使用者個人資料。

MainActivity.kt

  1. 建立 MainActivity 時,ViewModel 會例項化,並載入名為 MainNavHost 的 Compose 函式。
  2. MainNavHost 會從 viewModel 的 billingConnectionSate Livedata 中建立的變數 isBillingConnected 開始,並觀察變更,因為 vieModel 執行個體化時,會將 billingConnectionSate 傳遞至 BillingClientWrapper 的 startBillingConnection 方法。

連線建立時,isBillingConnected 會設為 true,如未連線,則會設為 false。

如果設為 False,系統會載入 LoadingScreen() Compose 函式,並在載入 true 時載入 Subscription 或設定檔函式。

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. 建立帳單連結後:

Compose navController 已執行個體化

val navController = rememberNavController()

然後,系統會收集 MainViewModel 中的資料流。

val productsForSale by viewModel.productsForSaleFlows.collectAsState(
   initial = MainState()
)

val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
   initial = listOf()
)

最後,系統會觀察到 viewModel 的 destinationScreen LiveData 變數。

系統會根據使用者目前的訂閱狀態,顯示對應的 Compose 函式。

val screen by viewModel.destinationScreen.observeAsState()
when (screen) {
   // User has a Basic Prepaid subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_monthly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_basic_yearly_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_BASIC_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable basic subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.BASIC_RENEWABLE_PROFILE -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.monthly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_premium_upgrade_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.basic_sub_message
       )
   }
   // User has a prepaid Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_PREPAID_PROFILE_SCREEN -> {
       UserProfile(
           buttonModels =
           listOf(
               ButtonModel(R.string.topup_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = null,
                           tag = PREPAID_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_monthly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.convert_to_premium_yearly_message) {
                   productsForSale.premiumProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_PREMIUM_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
           ),
           tag = PREPAID_PREMIUM_PLANS_TAG,
           profileTextStringResource = null
       )
   }
   // User has a renewable Premium subscription
   // the corresponding profile is loaded.
   MainViewModel.DestinationScreen.PREMIUM_RENEWABLE_PROFILE -> {
       UserProfile(
           listOf(
               ButtonModel(R.string.monthly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = MONTHLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.yearly_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = YEARLY_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               },
               ButtonModel(R.string.prepaid_basic_downgrade_message) {
                   productsForSale.basicProductDetails?.let {
                       viewModel.buy(
                           productDetails = it,
                           currentPurchases = currentPurchases,
                           tag = PREPAID_BASIC_PLANS_TAG,
                           activity = activity
                       )
                   }
               }
           ),
           tag = null,
           profileTextStringResource = R.string.premium_sub_message
       )
   }
   // User has no current subscription - the subscription composable
   // is loaded.
   MainViewModel.DestinationScreen.SUBSCRIPTIONS_OPTIONS_SCREEN -> {
       SubscriptionNavigationComponent(
           productsForSale = productsForSale,
           navController = navController,
           viewModel = viewModel
       )
   }
}

8. 解決方案程式碼

完整解決方案程式碼請見解決方案模組。

9. 恭喜

恭喜,您已成功將 Google Play 帳款服務程式庫 5.0.0 版訂閱產品整合至非常簡單的應用程式!

如需較複雜的應用程式 (含安全的伺服器設定) 的書面版本,請參閱官方範例

其他資訊