借助 Play 结算库 5 在应用内销售订阅内容

1. 简介

Google Play 结算系统是一项可让您在 Android 应用中销售数字商品和内容的服务。这是您销售应用内商品并利用应用变现的最直接方式。此 Codelab 将向您介绍如何使用 Google Play 结算库在项目中销售订阅,在将购买交易与应用的其余部分集成时,可以封装购买细节。

还介绍了与订阅相关的概念,例如基础方案、优惠、标签和预付费方案。如需详细了解 Google Play 结算服务上的订阅,您可以访问我们的帮助中心

构建内容

在此 Codelab 中,您会将最新的结算库(版本 5.0.0)添加到一个简单的基于订阅的个人资料应用中。该应用已为您构建,您只需添加结算部分即可。如图 1 所示,在该应用中,用户通过两种可再生订阅产品(基本版和付费版)或不可续订的预付费方案来订阅任何基础方案和/或优惠。仅此而已。基础方案分别提供包月和包年订阅。用户可以升级或降级预付费订阅,也可以将预付费订阅转换为可续订订阅。

d7dba51f800a6cc4.png 2220c15b849d2ead

如需将 Google Play 结算库集成到您的应用中,您需要创建以下内容:

  • BillingClientWrapper - BillingClient 库的封装容器。它旨在封装与 Play 结算库的 BillingClient 的互动,但这在您自己的集成中并非必需。
  • SubscriptionDataRepository - 应用的结算代码库,其中包含应用的订阅商品目录(即待售商品)列表,以及有助于收集购买交易状态和商品详情的 ShareFlow 变量列表
  • MainViewModel - 一个 ViewModel,应用的其余部分通过它与结算代码库进行通信。这有助于使用各种购买方法在界面中启动结算流程。

完成后,应用的架构应如下图所示:

c83bc759f32b0a63.png

学习内容

  • 如何集成 Play 结算库
  • 如何通过 Play 管理中心创建订阅商品、基础方案、优惠和标签
  • 如何从应用中检索可用的基础方案和优惠
  • 如何使用适当的参数启动结算流程
  • 如何提供预付费订阅产品

此 Codelab 将重点介绍 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 代码库中。首先,您需要获取代码,并在您喜爱的开发环境中将其打开。对于此 Codelab,我们建议使用 Android Studio。

入门代码存储在 GitHub 代码库中。您可以通过以下命令克隆代码库:

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

3. 基础

我们从何处入手?

我们首先为本 Codelab 设计了一个基本用户个人资料应用。代码已经过简化,可显示我们想要说明的概念,但并非专为生产环境而设计。如果您选择在正式版应用中重复使用此代码的任何部分,请务必遵循最佳实践并全面测试您的所有代码。

将项目导入 Android Studio

PlayBillingCodelab 是不包含 Google Play 结算服务实现的基础应用。启动 Android Studio,然后选择 Open > billing/PlayBillingCodelab 以导入 billing-Codelab

该项目包含两个模块:

  • start 具有框架应用,但缺少所需的依赖项以及您需要实现的所有方法。
  • finished 具有已完成的项目,可以在您遇到困难时用作指导。

该应用包含八个类文件:BillingClientWrapperSubscriptionDataRepositoryComposablesMainStateMainViewModelMainViewModelFactoryMainActivity

  • BillingClientWrapper 是一种封装容器,用于隔离 Google Play 结算服务所需的 [BillingClient] 方法,该方法需要简单实现,并将响应发送到数据存储区进行处理。
  • SubscriptionDataRepository 用于抽象化 Google Play 结算服务数据源(即结算客户端库),并将 BillingClientWrapper 中发出的 StateFlow 数据转换为数据流。
  • ButtonModel 是用于在界面中构建按钮的数据类。
  • Composables会将界面的所有可组合方法提取到一个类中。
  • MainState 是用于状态管理的数据类。
  • MainViewModel 用于保存界面中使用的结算相关数据和状态。它将 SubscriptionDataRepository 中的所有流合并为一个状态对象。
  • MainActivity 是用于为界面加载可组合项的主 activity 类。
  • 常量是指包含多个类使用的常量的对象。

Gradle

您需要添加 Gradle 依赖项,才能将 Google Play 结算服务添加到您的应用中。打开应用模块的 build.gradle 文件,并添加以下内容:

dependencies {
    val billing_version = "5.0.0"

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

Google Play 管理中心

在此 Codelab 中,您需要在 Google Play 管理中心的“订阅”部分创建以下两种订阅产品:

  • 1 项基本订阅(产品 ID 为 up_basic_sub

该商品应具有 3 个带相关标签的基础方案(2 个自动续订型和 1 个预付费方案)和 1 个带关联标签的基础方案:1 个月度基本订阅,标签为 monthlybasic;1 个基本年度订阅,标签为 yearlybasic;1 个预付费订阅,标签为 prepaidbasic

您可以在基础方案中添加优惠。优惠会沿用其关联的基础方案中的标签。

  • 1 项产品 ID 为 up_premium_sub 的付费订阅

该商品应有 3 个基础方案(2 个自动续订型订阅和 1 个预付费方案)并带有关联标签:1 个月度基本订阅,标签为 monthlypremium,1 个基本年度订阅,标签为 yearlypremium,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 管理中心内设置的产品尚未激活,或者您未在任何发布轨道中发布具有结算客户端依赖项的 build。

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. 终止结算连接

最后,当 activity 被销毁时,您需要终止与 Google Play 的连接,因此系统会调用 endConnection() 来执行此操作。

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

BillingClientWrapper 中,来自 QueryPurchasesAsyncQueryProductDetails 的响应会分别发布到 MutableStateFlow _purchases_productWithProductDetails,它们在班级外公开,并且包含购买和 productWithProductDetails

SubscriptionDataRepository 中,系统会根据退回的购买交易对应的商品将购买交易处理到三个 Flow 中:hasRenewableBasichasPrepaidBasichasRenewablePremiumhasPremiumPrepaid

此外,productWithProductDetails 也会处理到相应的 basicProductDetailspremiumProductDetails Flow。

6. MainViewModel

困难的部分已经完成了。现在,您将定义 MainViewModel,这只是您的客户端的公共接口,因此客户无需了解 BillingClientWrapperSubscriptionDataRepository 的内部构件。

首先,在 MainViewModel 中,我们在初始化 viewModel 时启动结算连接。

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

然后,按照 repo 类中处理的要求,仓库中的数据流分别被合并到 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 方法。

这是为了在关联的 activity 被销毁时终止当前的结算连接。

7. 界面

现在,该使用您在界面中构建的所有内容了。为此,您将使用可组合项和 MainActivity 类。

Composables.kt

Composables 类已完全提供,并定义了用于渲染界面的所有 Compose 函数以及这些函数之间的导航机制。

Subscriptions 函数会显示两个按钮:Basic SubscriptionPremium Subscription

Basic SubscriptionPremium Subscription 均会加载新的 Compose 方法,以显示三个相应的基础方案:包月、包年和预付费。

然后,针对用户可能拥有的特定订阅,有三种可能的配置文件组合函数:可再生的基本版、可再续的 Premium 以及预付费基本版或预付费高级版。

  • 如果用户拥有基本版订阅,则可以通过基本个人资料升级到按月、按年或预付费的付费订阅。
  • 相反,如果用户拥有付费订阅,则可以降级为按月、按年或预付费的基本订阅。
  • 当用户拥有预付费订阅后,可以使用“充值”按钮为订阅充值,或将预付费订阅转换为相应的自动续订型基础方案。

最后是“正在加载”屏幕函数,可在与 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 订阅产品集成到一个非常简单的应用中!

如需查看具有安全服务器设置的更复杂应用的文档版本,请参阅官方示例

深入阅读