Play Billing Library 5 を使用してアプリ内で定期購入を販売する

1. はじめに

Google Play の課金システムは、Android アプリ内でデジタル商品とデジタル コンテンツを販売するためのサービスです。アプリ内アイテムを販売することで、アプリを収益化できます。この Codelab では、Google Play Billing Library を使用して、アプリ内で定期購入を統合する際に重要なことをカプセル化する方法で、定期購入を販売する方法について説明します。

また、基本プラン、特典、タグ、前払いプランなど、定期購入に関連するコンセプトも紹介しています。Google Play 請求サービスに関する定期購入について詳しくは、ヘルプセンターをご覧ください。

作成するアプリの概要

この Codelab では、シンプルな Billing Library に最新の Billing Library(バージョン 5.0.0)を追加します。このアプリはすでに用意されているため、課金部分を追加するだけです。図 1 に示すように、ユーザーは 2 つの再生可能な定期購入プロダクト(ベーシックとプレミアム)を通じて提供される基本プランや特典、あるいは更新不可の前払い定期購入のいずれかに申し込みます。以上です。基本プランは、それぞれ月単位および年単位のサブスクリプションです。ユーザーは前払いの定期購入をアップグレード、ダウングレード、または更新可能に移行できます。

d7dba51f800a6cc4.png 2220C15B849D2EAD.PNG

Google Play Billing Library をアプリに組み込むには、以下を作成します。

  • BillingClientWrapper: BillingClient ライブラリのラッパー。Play Billing Library の BillingClient とのインタラクションはカプセル化されますが、独自の統合では必要とされません。
  • SubscriptionDataRepository - アプリの定期購入商品のインベントリ(販売対象)のリストと、購入ステータスや商品の詳細を収集するのに役立つ ShareFlow 変数のリストを含む、アプリの請求リポジトリ
  • MainViewModel - アプリの他の部分が課金リポジトリと通信するための ViewModel。UI でさまざまな購入方法を使用して請求フローを起動するのに役立ちます。

完了すると、アプリのアーキテクチャは次の図のようになります。

c83bc759f32b0a63.png

ラボの内容

  • Play 請求サービス ライブラリを統合する方法
  • Play Console で定期購入商品、基本プラン、特典、タグを作成する方法
  • 利用可能な基本プランと特典をアプリから取得する方法
  • 適切なパラメータで請求フローを開始する方法
  • 前払いの定期購入商品を提供する方法

この 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 をインポートします。

プロジェクトには 2 つのパッケージがあります。

  • Codelab にはスケルトン アプリがありますが、必要な依存関係と、実装に必要なすべてのメソッドがありません
  • ソリューションにはプロジェクトが完成しており、行き詰ったときのガイドとして利用できる

アプリは、BillingClientWrapperSubscriptionDataRepositoryComposablesMainStateMainViewModelMainViewModelFactoryMainActivity の 7 つのクラスファイルで構成されています。

  • BillingClientwrapper は、シンプルな実装で必要とされる Google Play 請求サービスの [BillingClient] メソッドを分離するラッパーで、データ リポジトリにレスポンスを送信して処理します。
  • SubscriptionDataRepository は、Google Play Billing データソース(Billing Client ライブラリ)を抽象化し、BillingClientwrapper で出力された StateFlow データを Flow に変換するために使用されます。
  • ButtonModel は、UI でボタンを作成するために使用されるデータクラスです。
  • コンポーザブルは、すべての UI のコンポーザブル メソッドを 1 つのクラスに抽出します。
  • MainState は状態管理用のデータクラスです。
  • MainViewModel は、UI で使用される請求関連のデータと状態を保持するために使用されます。SubscriptionDataRepository 内のすべてのフローを 1 つの状態オブジェクトに結合します。
  • MainViewModelFactory は MainViewModel のインスタンス化に使用される ViewModelProviders.Factory インターフェースの実装です。
  • 定数は、複数のクラスで使用される定数を持つオブジェクトです。
  • MainActivity は、ユーザー インターフェースのコンポーザブルを読み込むメイン アクティビティ クラスです。

Gradle

Google Play 請求サービスをアプリに追加するには、Gradle 依存関係を追加する必要があります。アプリ モジュールの build.gradle ファイルを開き、以下を追加します。

dependencies {
    val billing_version = "5.0.0"

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

コンソール

この Codelab では、以下の 2 つの定期購入サービスを Google Play Console で作成する必要があります。

  • 商品 ID up_basic_sub の基本定期購入 1 件

3 つの基本プラン(自動更新 2 つと前払い 1 つ)と、それに関連付けられたタグ(monthlybasic タグを含む基本定期購入 1 つ、タグ yearlybasic を含む基本定期購入 1 つ、タグ prepaidbasic 付きの定期購入 1 つ)が必要です

基本プランに特典を追加できます。特典は関連付けられた基本プランからタグを継承します。

  • プロダクト ID up_premium_sub のプレミアム サブスクリプション 1 件

3 つの基本プラン(2 つの自動更新と 1 つの前払い)とそれに関連付けられたタグ(monthlypremium タグを含む基本定期購入 1 つ、タグ yearlypremium を含む基本定期購入 1 つ、タグ prepaidpremium 付き)1 つが必要です

基本プランに特典を追加できます。特典は関連付けられた基本プランからタグを継承します。

定期購入商品、基本プラン、特典、タグの作成方法について詳しくは、こちらの Google Play ヘルプセンターをご覧ください。

4. 請求クライアントの設定

このセクションでは、BillingClientWrapper クラスを操作します。

最終的に、Billing Client のインスタンス化に必要なものと、関連するすべてのメソッドが準備されます。

  1. BillingClient を初期化する

Google Play Billing Library への依存関係を追加したら、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 Console で設定されたプロダクトが有効化されていないか、いずれかのリリース トラックで請求クライアントの依存関係を含むビルドを公開していない可能性があります。

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 に投稿され、クラスの外部(purchase と productWithProductDetails)で公開されます。

SubscriptionDataRepository では、返された購入商品(hasRenewableBasichasPrepaidBasichasRenewablePremiumhasPremiumPrepaid)に基づいて、3 つのフローに処理されます。

さらに、productWithProductDetails はそれぞれの basicProductDetails フローと premiumProductDetails フローに処理されます。

6. MainViewModel

これで、次に、BillingClientWrapperSubscriptionDataRepository の内部を理解する必要がないように、クライアントのパブリック インターフェースである MainViewModel を定義します。

まず、MainViewModel で、viewModel が初期化されたときに請求接続を開始します。

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

次に、リポジトリからのフローは、それぞれリポジトリ クラスで処理される productsForSaleFlows(利用可能なプロダクトの場合)と userCurrentSubscriptionFlow(ユーザーの現在およびアクティブなサブスクリプションの場合)に結合されます。

現在の購入のリストも UI で 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 Billing Library バージョン 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 をビルドする必要があります。

これには次の 2 つの方法があります。

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. 購入方法

purchase メソッドは、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. 課金接続を終了する

最後に、BillingClientWrapperterminateBillingConnection メソッドが ViewModel の onCleared() で呼び出されます。

これは、関連付けられているアクティビティが破棄されたときに、現在の請求接続を終了します。

7. UI

次は、UI で作成したすべての機能を使用します。そのために、コンポーザブル クラスと MainActivity クラスを使用します。

Composables.kt

Composables クラスは完全に提供され、UI とそれらの間のナビゲーション メカニズムのレンダリングに使用されるすべての Compose 関数を定義します。

Subscriptions 関数は、Basic SubscriptionPremium Subscription の 2 つのボタンを表示します。

Basic SubscriptionPremium Subscription はそれぞれ、3 つの基本プラン(月単位、年単位、前払い)を示す新しい Compose メソッドを読み込みます。

ユーザーが特定の定期購入で作成できるプロフィール作成機能として、再生可能なベーシック、再生可能なプレミアム、プリペイドベーシックまたはプリペイドプレミアムの 3 つがあります。

  • ユーザーが基本サブスクリプションを利用している場合、ベーシック プロフィールでは、月単位、年単位、前払いのプレミアム サブスクリプションのいずれかにアップグレードできます。
  • 逆に、プレミアム サブスクリプションの場合は、月単位、年単位、前払いの基本のいずれかにダウングレードできます。
  • 前払いの定期購入を利用している場合、[チャージ] ボタンを使用して定期購入にチャージするか、前払いの定期購入を対応する自動更新の基本プランに変換することができます。

読み込み画面には、Google Play への接続時とユーザー プロフィールのレンダリング時に使用する読み込み機能があります。

MainActivity.kt

  1. MainActivity が作成されると、viewModel がインスタンス化され、MainNavHost という作成関数が読み込まれます。
  2. MainNavHost は、viewModel の billingConnectionSate Livedata から作成された変数 isBillingConnected で始まり、変更を監視されます。これは、vieModel がインスタンス化されると、billingConnectionSateBillingClientWrapper の startBillingConnection メソッドに渡すためです。

接続が確立されている場合は 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()
)

最後に、viewModel の destinationScreen LiveData 変数を監視します。

ユーザーの現在のサブスクリプション ステータスに基づいて、対応する作成関数がレンダリングされます。

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 の定期購入サービスを非常にシンプルなアプリに統合できました。

セキュリティで保護されたサーバーを備えた、より高度なバージョンのアプリについては、公式サンプルをご覧ください。

参考資料