Продавайте подписки в приложении с помощью Play Billing Library 5.

1. Введение

Биллинговая система Google Play – это сервис, который позволяет продавать цифровые продукты и контент в вашем приложении Android. Это самый прямой способ продавать встроенные в приложение продукты и монетизировать свое приложение. В этой кодовой лаборатории показано, как использовать Библиотеку выставления счетов Google Play для продажи подписок в вашем проекте таким образом, чтобы инкапсулировать мельчайшие детали при интеграции покупок с остальной частью вашего приложения.

В нем также представлены концепции, связанные с подпиской, такие как базовые планы, предложения, теги и планы с предоплатой. Чтобы узнать больше о подписках в Google Play Billing, посетите наш Справочный центр .

Что ты построишь

В этой лаборатории кода вы добавите последнюю версию библиотеки выставления счетов (версия 5.0.0) в простое приложение профиля на основе подписки. Приложение уже создано для вас, поэтому вам просто нужно добавить платежную часть. Как показано на рисунке 1, в этом приложении пользователь подписывается на любой из базовых планов и/или предложений, предлагаемых через два возобновляемых продукта подписки (базовый и премиум) или на невозобновляемую предоплату. Вот и все. Базовые планы представляют собой ежемесячную и годовую подписки соответственно. Пользователь может обновить, понизить или преобразовать предоплаченную подписку в возобновляемую.

d7dba51f800a6cc4.png2220c15b849d2ead.png

Чтобы включить библиотеку платежей Google Play в свое приложение, вам необходимо создать следующее:

  • BillingClientWrapper — оболочка для библиотеки BillingClient. Он предназначен для инкапсуляции взаимодействия с BillingClient библиотеки биллинга Play, но это не требуется для вашей собственной интеграции.
  • SubscriptionDataRepository — хранилище счетов для вашего приложения, которое содержит список инвентаря продуктов подписки приложения (т. е. того, что продается), а также список переменных ShareFlow, который помогает собирать состояние покупок и сведения о продуктах.
  • MainViewModel — модель представления, через которую остальная часть вашего приложения взаимодействует с репозиторием биллинга. Это помогает запустить процесс выставления счетов в пользовательском интерфейсе, используя различные способы покупки.

По завершении архитектура вашего приложения должна выглядеть примерно так, как показано на рисунке ниже:

c83bc759f32b0a63.png

Что вы узнаете

  • Как интегрировать платежную библиотеку Play
  • Как создавать продукты по подписке, базовые планы, предложения и теги с помощью Play Console
  • Как получить доступные базовые планы и предложения из приложения
  • Как запустить поток биллинга с соответствующими параметрами
  • Как предлагать продукты по предоплаченной подписке

Эта лаборатория кода ориентирована на биллинг в Google Play. Нерелевантные концепции и блоки кода замалчиваются и предоставляются для простого копирования и вставки.

Что вам понадобится

  • Последняя версия Android Studio (>= Arctic Fox | 2020.3.1)
  • Устройство Android с Android 8.0 или более поздней версии.
  • Пример кода, предоставленный вам на GitHub (инструкции в последующих разделах).
  • Умеренные знания разработки Android в Android Studio.
  • Знание того, как опубликовать приложение в Google Play Store.
  • Умеренный опыт написания кода на 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 Billing. Запустите Android Studio и импортируйте Billing-Codelab, выбрав Open > billing/PlayBillingCodelab

Проект состоит из двух модулей:

  • В start есть скелетное приложение, но в нем отсутствуют необходимые зависимости и все методы, которые необходимо реализовать.
  • «Готово» содержит завершенный проект и может служить подсказкой, когда вы застряли.

Приложение состоит из восьми файлов классов: BillingClientWrapper , SubscriptionDataRepository , Composables , MainState , MainViewModel , MainViewModelFactory и MainActivity .

  • BillingClientWrapper — это оболочка, которая изолирует методы [BillingClient] Google Play Billing, необходимые для простой реализации, и отправляет ответы в репозиторий данных для обработки.
  • SubscriptionDataRepository используется для абстрагирования источника данных о выставлении счетов в Google Play (т. е. библиотеки клиента выставления счетов) и преобразует данные StateFlow, созданные в BillingClientWrapper, в потоки.
  • ButtonModel — это класс данных, используемый для создания кнопок в пользовательском интерфейсе.
  • Composables извлекает все компонуемые методы пользовательского интерфейса в один класс.
  • MainState — это класс данных для управления состоянием.
  • MainViewModel используется для хранения данных и состояний, связанных с выставлением счетов, используемых в пользовательском интерфейсе. Он объединяет все потоки в SubscriptionDataRepository в один объект состояния.
  • MainActivity — это основной класс активности, который загружает Composables для пользовательского интерфейса.
  • Константы — это объект, константы которого используются несколькими классами.

Градл

Вам необходимо добавить зависимость Gradle, чтобы добавить биллинг Google Play в ваше приложение. Откройте файл build.gradle модуля приложения и добавьте следующее:

dependencies {
    val billing_version = "5.0.0"

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

Консоль Google Play

Для целей этой лаборатории кода вам необходимо создать следующие два продукта по подписке в разделе подписок консоли Google Play :

  • 1 базовая подписка с идентификатором продукта up_basic_sub

Продукт должен иметь 3 базовых плана (2 с автоматическим продлением и 1 с предоплатой) со соответствующими тегами: 1 базовая ежемесячная подписка с тегом monthlybasic , 1 годовая базовая подписка с тегом yearlybasic и 1 предоплаченная подписка с тегом prepaidbasic

Вы можете добавлять предложения в базовые планы. Предложения унаследуют теги от связанных с ними базовых планов.

  • 1 премиум-подписка с идентификатором продукта up_premium_sub

Продукт должен иметь 3 базовых плана (2 с автоматическим продлением и 1 с предоплатой) со соответствующими тегами: 1 базовая ежемесячная подписка с тегом monthlypremium , 1 годовая базовая подписка с тегом yearlypremium и 1 предоплаченная подписка с тегом prepaidpremium

a9f6fd6e70e69fed.png

Вы можете добавлять предложения в базовые планы. Предложения унаследуют теги от связанных с ними базовых планов.

Более подробную информацию о том, как создавать продукты по подписке, базовые планы, предложения и теги, можно найти в Справочном центре Google Play .

4. Настройка Биллингового клиента

В этом разделе вы будете работать с классом BillingClientWrapper .

К концу у вас будет все необходимое для создания экземпляра Биллингового клиента и всех связанных с ним методов.

  1. Инициализация BillingClient

После того как мы добавили зависимость от библиотеки Google Play Billing, нам необходимо инициализировать экземпляр 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

Примечание. Этот метод передает результат запроса в Map на _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 содержит атрибуты Product 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. Репозиторий данных подписки.

В BillingClientWrapper ответы от QueryPurchasesAsync и QueryProductDetails публикуются соответственно в MutableStateFlow _purchases и _productWithProductDetails , которые доступны за пределами класса с покупками и productWithProductDetails .

В SubscriptionDataRepository покупки обрабатываются в три потока на основе возвращенного продукта покупки: hasRenewableBasic , hasPrepaidBasic , hasRenewablePremium и hasPremiumPrepaid .

Кроме того, productWithProductDetails обрабатывается в соответствующие потоки basicProductDetails и premiumProductDetails .

6. Модель MainViewModel

Самое сложное сделано. Теперь вы собираетесь определить MainViewModel , который является общедоступным интерфейсом для ваших клиентов, поэтому им не нужно знать внутреннее устройство BillingClientWrapper и SubscriptionDataRepository .

Сначала в 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 собирается в блоке инициализации, а значение отправляется в объект MutableLiveData с именем _destinationScreen .

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() .

Метод возвращает токен идентификатора предложения выбранного предложения.

Эта реализация просто возвращает предложения с самой низкой ценой с точки зрения набора 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. Метод покупки

Метод покупки использует launchBillingFlow() и BillingFlowParams класса BillingClientWrapper для запуска покупок.

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. Завершить платежное соединение

Наконец, метод terminateBillingConnection BillingClientWrapper вызывается в onCleared() ViewModel.

Это означает, что текущее соединение для выставления счетов будет прекращено, когда связанное с ним действие будет уничтожено.

7. Пользовательский интерфейс

Теперь пришло время использовать все, что вы создали в пользовательском интерфейсе. Чтобы помочь в этом, вы будете работать с классами Composables и MainActivity.

Composables.kt

Класс Composables полностью предоставляется и определяет все функции Compose, используемые для визуализации пользовательского интерфейса, и механизм навигации между ними.

Функция Subscriptions отображает две кнопки: Basic Subscription и Premium Subscription .

Каждая Basic Subscription и Premium Subscription загружает новые методы Compose, которые отображают три соответствующих базовых плана: ежемесячный, годовой и предоплаченный.

Кроме того, существует три возможных функции создания профиля, каждая для конкретной подписки, которую может иметь пользователь: возобновляемый базовый, возобновляемый премиальный профиль и либо предоплаченный базовый профиль, либо предоплаченный премиальный профиль.

  • Если у пользователя есть базовая подписка, базовый профиль позволяет ему перейти на ежемесячную, годовую подписку или премиальную подписку с предоплатой.
  • И наоборот, если у пользователя есть премиум-подписка, он может перейти на ежемесячную, годовую или предоплаченную базовую подписку.
  • Если у пользователя есть предоплаченная подписка, он может пополнить свою подписку с помощью кнопки пополнения или преобразовать свою предоплаченную подписку в соответствующий базовый план с автоматическим продлением.

Наконец, есть функция экрана загрузки, которая используется при подключении к Google Play и при отображении профиля пользователя.

MainActivity.kt

  1. При создании MainActivity создается экземпляр viewModel и загружается функция компоновки MainNavHost .
  2. MainNavHost начинается с переменной isBillingConnected , созданной на основе данных billingConnectionSate Livedata viewModel и наблюдаемой на предмет изменений, поскольку при создании экземпляра vieModel она передает billingConnectionSate в метод startBillingConnection BillingClientWrapper .

isBillingConnected имеет значение true, если соединение установлено, и false, если нет.

Если значение false, загружается функция компоновки LoadingScreen() , а если значение 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()
)

Наконец, наблюдается переменная destinationScreen LiveData ViewModel.

В зависимости от текущего статуса подписки пользователя отображается соответствующая функция создания.

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 Billing Library 5.0.0 в очень простое приложение!

Документированную версию более сложного приложения с безопасной настройкой сервера см. в официальном образце .

дальнейшее чтение