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

1. はじめに

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

また、基本プラン、特典、タグ、プリペイド プランなど、定期購入関連の概念についても説明します。Google Play 請求サービスでの定期購入について詳しくは、ヘルプセンターをご参照ください。

作成するアプリの概要

この Codelab では、シンプルなサブスクリプション ベースのプロファイル アプリに、最新の Billing ライブラリ(バージョン 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 Billing Library を統合する方法
  • Google Play Console で定期購入商品、基本プラン、特典、タグを作成する方法
  • 利用可能な基本プランと特典をアプリから取得する方法
  • 適切なパラメータを使用して請求フローを開始する方法
  • 前払い定期購入商品を提供する方法

この Codelab では、Google Play 請求サービスを中心に説明します。関連のない概念やコードブロックについては詳しく触れず、コードはコピーして貼るだけの状態で提供されています。

必要なもの

  • 最新バージョンの Android Studio(Arctic Fox | 2020.3.1 以降)
  • Android 8.0 以降を搭載した Android デバイス
  • GitHub で提供されているサンプルコード(後のセクションで手順を説明します)
  • Android Studio での Android 開発に関する中程度の知識
  • Google Play ストアにアプリを公開する方法に関する知識
  • Kotlin コードを記述した経験(中程度)
  • Google Play Billing Library バージョン 5.0.0

2. 設定方法

GitHub からコードを取得する

このプロジェクトで必要となるすべてのものは Git リポジトリに用意されています。まず、コードを取得して、お好みの開発環境で開きます。この Codelab では、Android Studio を使用することをおすすめします。

作業を開始するためのコードは GitHub リポジトリに保存されています。リポジトリのクローンを作成するには、次のコマンドを使用します。

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

3. 基礎知識

出発点

出発点は、この Codelab 用に設計された基本的なユーザー プロファイル アプリです。このコードは、説明したいコンセプトを示すために簡略化されており、本番環境での使用は想定されていません。このコードの一部を製品版アプリで再利用する場合は、ベスト プラクティスに沿って、すべてのコードを十分にテストしてください。

プロジェクトを Android Studio にインポートする

PlayBillingCodelab は、Google Play 請求サービスを実装していないベースアプリです。Android Studio を起動し、Open > billing/PlayBillingCodelab を選択して billing-codelab をインポートします

このプロジェクトには次の 2 つのモジュールがあります。

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

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

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

Gradle

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

dependencies {
    val billing_version = "5.0.0"

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

Google Play コンソール

この Codelab では、Google Play Console の [定期購入] セクションで次の 2 つの定期購入商品を作成する必要があります。

  • アイテム 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 件

a9f6fd6e70e69fed.png

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

定期購入商品、基本プラン、特典、タグの作成方法について詳しくは、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 to _productWithProductDetails」に出力します。

また、このクエリは ProductDetails を返すことが想定されています。表示されない場合は、Google 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 に投稿され、購入と productWithProductDetails でクラスの外部に公開されます。

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

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

6. MainViewModel

難しい部分はこれで完了です。次に、MainViewModel を定義します。これはクライアント用の公開インターフェースにすぎないため、BillingClientWrapperSubscriptionDataRepository の内部を認識する必要はありません。

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

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

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

現在の購入のリストは、currentPurchasesFlow を使用して UI でも利用できます。

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 ビルダー

特定のアイテムの購入フローを開始するにはBilingFlowParams を作成するには、ProductDetails と選択したオファーのトークンを設定して使用する必要があります。

これには次の 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. 購入方法

購入メソッドでは、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 で作成したものをすべて活用しましょう。そのために、Composable クラスと MainActivity クラスを使用します。

Composables.kt

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

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

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

次に、ユーザーが利用する特定の定期購入ごとに、更新可能なベーシック、更新可能なプレミアム、プリペイド ベーシックまたはプリペイド プレミアムの 3 つのプロファイル作成機能を使用できます。

  • ユーザーが Basic プランに加入している場合は、Basic プロファイルを使用して、月単位、年単位、前払いの Premium プランにアップグレードできます。
  • 逆に、プレミアム定期購入を利用している場合は、月単位、年単位、または前払いのベーシックの定期購入にダウングレードできます。
  • ユーザーが前払い定期購入を利用している場合、チャージボタンを使用して定期購入をチャージしたり、前払い定期購入を対応する自動更新の基本プランに変換したりできます。

最後に、読み込み画面機能が Google Play に接続されるときや、ユーザー プロフィールがレンダリングされるときに使用されます。

MainActivity.kt

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

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 変数を監視します。

ユーザーの現在のサブスクリプション ステータスに基づいて、対応する 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 Billing Library 5.0.0 の定期購入商品を、非常にシンプルなアプリに統合できました。

安全なサーバー設定をした高度なアプリのドキュメント バージョンについては、公式サンプルをご覧ください。

参考資料