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

1. 简介

Google Play 结算系统是一项服务,可让您在 Android 应用中销售数字商品和内容。这是通过应用内商品销售应用的最直接方式。此 Codelab 将向您展示如何使用 Google Play 结算库在项目中销售订阅,从而在将购买与其余应用集成时将包含详细的交易信息。

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

构建内容

在此 Codelab 中,您将把最新的结算库(5.0.0 版)添加到一个基于订阅的简单个人资料应用中。该应用已经为您构建好了,因此您只需添加结算部分即可。如图 1 所示,在此应用中,用户注册通过两个可续订订阅产品(基本版和高级版)提供的任何基础方案和/或优惠,或注册为不可续订的预付费方案。就这些了。基础方案为按月订阅和按年订阅。用户可以将预付费订阅升级到升级、降级或转换为可续订的订阅。

d7dba51f800a6cc4.png 2220c15b849d2ead.png

若要在您的应用中集成 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 代码的经验

2. 准备工作

从 GitHub 获取代码

我们已将此项目所需的所有内容放入 Git 代码库中。首先,您需要获取代码并在您喜欢的开发环境中打开。对于此 Codelab,我们建议使用 Android Studio。

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

git clone https://github.com/android/play-billing-codelabs

3.基础

我们从何处入手?

我们的起点是针对此 Codelab 设计的基本用户个人资料应用。代码经过简化,可显示我们要演示的概念,而且该代码并非专为生产环境而设计。如果您选择在正式版应用中重复使用此代码的任何部分,请务必遵循最佳做法并全面测试所有代码。

将项目导入 Android Studio

Billing-codelab 是不包含 Google Play 结算服务实现的基础应用。选择 **Open > billing-codelab/build.gradle**,启动 Android Studio 并导入结算 Codelab

该项目有两个软件包:

  • codelab 具有框架应用,但缺少必需的依赖项以及您需要实现的所有方法
  • 解决方案包含已完成的项目,当您遇到困难时,可以为您提供指导

该应用由七个类文件组成:BillingClientWrapperSubscriptionDataRepositoryComposablesMainStateMainViewModelMainViewModelFactoryMainActivity

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

Gradle

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

dependencies {
    val billing_version = "5.0.0"

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

控制台

在此 Codelab 中,您需要在 Google Play 管理中心内创建以下两个订阅产品:

  • 1 个商品 ID 为 up_basic_sub 的基本订阅

monthlybasic

您可以向基础方案添加优惠。优惠将沿用其关联基础方案中的标记。

  • 1 个商品 ID 为 up_premium_sub 的高级订阅

该产品应具有 3 种基础方案(2 项自动续订和 1 项预付费相关):含 1 个标签为 monthlypremium 的包月基本订阅、1 个含 yearlypremium 标签的年度基本订阅,以及 1 项带有标签 prepaidpremium 的预付费订阅

您可以向基础方案添加优惠。优惠将沿用其关联基础方案中的标记。

如需详细了解如何创建订阅产品、基础方案、优惠和标签,请点击此处访问 Google Play 帮助中心。

4.结算客户端设置

在这一部分中,您将使用 BillingClientWrapper 类。

最后,您将拥有实例化结算客户端所需的一切,以及所有相关方法。

  1. 初始化 BillingClient

添加 Google Play 结算库的依赖项后,我们需要初始化 BillingClient 实例。

BillingClient 封装容器.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. 与 Google Play 建立连接

您创建 BillingClient 后,我们需要与 Google Play 建立连接。

为了连接到 Google Play,我们会调用 startConnection()。连接过程是异步进行的,我们需要实现 BillingClientStateListener 以在客户端设置完成后且它准备好发出进一步的请求时接收回调。

BillingClient 封装容器.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() 查询用户之前完成的购买交易。

BillingClient 封装容器.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()。在查询商品详情之前,查询商品是非常重要的一步,因为查询会返回本地化的商品信息。

BillingClient 封装容器.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。

BillingClient 封装容器.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 启动购买流程。

BillingClient 封装容器.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 对象确定是需要处理的新购买交易,还是不需要进一步处理的现有购买交易。

对于订阅购买交易,处理过程与确认新购买交易类似。

BillingClient 封装容器.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。

BillingClient 封装容器.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() 来终止它。

BillingClient 封装容器.kt

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

5. SubscriptionDataRepository

BillingClientWrapper 中,QueryPurchasesAsyncQueryProductDetails 的响应会分别发布到 MutableStateFlow _purchases_productWithProductDetails,这些类和类通过购买和 productWithProductDetails 公开。

SubscriptionDataRepository 中,系统会根据返回的购买产品将购买处理成三个流:hasRenewableBasichasPrepaidBasichasRenewablePremiumhasPremiumPrepaid

此外,productWithProductDetails 会被处理为相应的 basicProductDetailspremiumProductDetails Flow。

6. MainViewModel

困难的部分到此就告一段落。现在,您将定义 MainViewModel,它只是客户端的公共接口,因此客户端无需了解 BillingClientWrapperSubscriptionDataRepository 的内部构件。

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

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 类。

可组合项.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 订阅产品集成到一个非常简单的应用中!

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

深入阅读