コード例:買い切り型
はい。今回のような 「一度購入すれば永久に機能解放される非消費型(買い切り)」 なら、Androidでは Google Play Billing Library を使います。
Apple側でいうと、
- App Store Connect → 非消費型(Non-Consumable)
- StoreKit 2 →
Product.PurchaseOption - Android / Google Play → One-time product(非消費型)
という対応関係です。Google Playでは現在、買い切り商品は「One-time product」と呼ばれ、その中に Consumable / Non-consumable があります。(Android Developers)
今回は例えば、以前作られていたテーブルメモアプリの
「行・列の追加機能を購入すると永久に解放」
というケースを想定して説明します。
1. Google Play Console側で商品を作る
まずGoogle Play Consoleでアプリを登録し、買い切り商品を作ります。
例えば商品IDを
premium_access
とします。
重要なのは Subscriptionではなく One-time product にすることです。
イメージとしては、
商品ID
premium_access
種類
One-time product
価格
¥500
です。
Google Playの現在の仕様では、1回限りの商品は ProductType.INAPP として取得します。
2. Android StudioにBilling Libraryを追加
app/build.gradle.kts に追加します。
現在のGoogle公式ドキュメントでは Billing Library 9.1.0 が案内されています。
dependencies {
implementation("com.android.billingclient:billing-ktx:9.1.0")
}
その後、
Sync Project with Gradle Files
を実行します。
3. BillingClientを作る
例えば専用の BillingManager を作ると管理しやすいです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
class BillingManager( private val context: Context ) { private var billingClient: BillingClient init { billingClient = BillingClient.newBuilder(context) .setListener { billingResult, purchases -> handlePurchaseResult(billingResult, purchases) } .enablePendingPurchases() .build() } fun startConnection() { billingClient.startConnection( object : BillingClientStateListener { override fun onBillingSetupFinished( billingResult: BillingResult ) { if ( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) { // Google Play Billingとの接続成功 } } override fun onBillingServiceDisconnected() { // 必要に応じて再接続 } } ) } private fun handlePurchaseResult( billingResult: BillingResult, purchases: List<Purchase>? ) { // 購入結果を処理 } } |
Googleも BillingClient をアプリ側の購入処理の中心として使用する構成を案内しています。
4. 商品情報を取得する
ここがAppleのStoreKit 2と少し違うところです。
Google Play Consoleで作った
premium_access
を指定して商品情報を取得します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
fun loadProduct() { val productList = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId("premium_access") .setProductType( BillingClient.ProductType.INAPP ) .build() ) val params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build() billingClient.queryProductDetailsAsync( params ) { billingResult, productDetailsResult -> if ( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) { val products = productDetailsResult.productDetailsList val product = products.firstOrNull() if (product != null) { // 商品情報取得成功 } } } } |
ここで、
ProductType.INAPP
なのがポイントです。
SUBS ではありません。
Google Playでは、
INAPP = 1回限りの商品
SUBS = サブスクリプション
です。
5. 価格を画面に表示する
取得した ProductDetails から価格を取得できます。
例えば、
val offer =
product.oneTimePurchaseOfferDetailsList?.firstOrNull()
val price = offer?.formattedPrice
なら、
¥500
のような、Google Play側でローカライズされた価格を取得できます。
したがって、アプリ側で
プレミアム機能
¥500
のように表示できます。
価格を自分で ¥500 とハードコードするのではなく、Google Playから取得した価格を表示するのがおすすめです。GoogleのAPIはローカライズされた商品情報を返します。
6. 「購入する」ボタンを押したら購入画面を出す
ここが実際の購入処理です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
fun purchase( activity: Activity, product: ProductDetails ) { val offer = product.oneTimePurchaseOfferDetailsList ?.firstOrNull() ?: return val productDetailsParams = BillingFlowParams.ProductDetailsParams .newBuilder() .setProductDetails(product) .setOfferToken(offer.offerToken) .build() val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList( listOf(productDetailsParams) ) .build() billingClient.launchBillingFlow( activity, billingFlowParams ) } |
するとGoogle Playの購入UIが表示されます。
つまりアプリ側では、
[プレミアム機能を購入 ¥500]
↓
Google Play購入画面
↓
購入完了
という流れになります。
launchBillingFlow() はメインスレッドから呼び出し、ProductDetails は事前にGoogle Playから取得したものを使用します。
7. 購入成功を受け取る
購入が完了すると、最初に設定した
.setListener { billingResult, purchases ->
handlePurchaseResult(billingResult, purchases)
}
が呼ばれます。
例えば、
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private fun handlePurchaseResult( billingResult: BillingResult, purchases: List<Purchase>? ) { if ( billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null ) { for (purchase in purchases) { processPurchase(purchase) } } else if ( billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED ) { // ユーザーがキャンセル } else { // その他のエラー } } |
8. 非消費型なので「consume」しない
ここが今回かなり重要です。
非消費型なら、
consumeAsync()
は 使いません。
例えば、
ゲームコイン100枚
のような消費型なら、
購入
↓
100コイン付与
↓
消費
↓
再購入可能
ですが、今回の
プレミアム機能
なら、
購入
↓
プレミアム機能解放
↓
永久に所有
です。
GoogleのOne-time productでも、非消費型は購入後もユーザーアカウントに恒久的に紐付く商品として扱われます。
9. 購入をAcknowledgeする
非消費型では購入後に Acknowledgement(購入承認) が必要です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
private fun processPurchase( purchase: Purchase ) { if ( purchase.purchaseState == Purchase.PurchaseState.PURCHASED ) { // プレミアム機能を解放 unlockPremium() if (!purchase.isAcknowledged) { val params = AcknowledgePurchaseParams .newBuilder() .setPurchaseToken( purchase.purchaseToken ) .build() billingClient.acknowledgePurchase( params ) { billingResult -> if ( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) { // 承認成功 } } } } } |
これはかなり重要です。
Google Playでは、購入を 3日以内に承認しないと自動返金され、購入が取り消される 仕様です。
10. 「購入済みか」を確認する
買い切りアプリで最も重要なのがこれです。
例えばユーザーが、
昨日 ¥500で購入
↓
今日アプリを起動
した場合、
プレミアム機能 ON
にしなければなりません。
そのため、アプリ起動時などにGoogle Playから購入済み商品を確認します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
fun checkPurchases() { val params = QueryPurchasesParams.newBuilder() .setProductType( BillingClient.ProductType.INAPP ) .build() billingClient.queryPurchasesAsync( params ) { billingResult, purchases -> if ( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) { val purchased = purchases.any { it.products.contains("premium_access") && it.purchaseState == Purchase.PurchaseState.PURCHASED } if (purchased) { unlockPremium() } } } } |
これによって、
Google Playアカウント
↓
premium_accessを購入済み
↓
Androidアプリ
↓
プレミアム機能ON
という仕組みにできます。
11. Jetpack Composeとの関係
ここはSwiftUI + StoreKit 2との違いを理解するとかなり分かりやすいです。
Billing処理そのものはComposeのViewに直接書かないほうがいいです。
例えば、
Compose UI
│
↓
ViewModel
│
↓
BillingManager
│
↓
Google Play Billing
という構成がおすすめです。
例えばViewModelで、
|
1 2 3 4 5 6 7 8 9 10 11 |
class PremiumViewModel( private val billingManager: BillingManager ) : ViewModel() { var isPremium by mutableStateOf(false) private set fun purchase(activity: Activity) { billingManager.purchasePremium(activity) } } |
そしてCompose側は、
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Composable fun PremiumScreen( viewModel: PremiumViewModel, activity: Activity ) { Button( onClick = { viewModel.purchase(activity) } ) { Text("プレミアム機能を購入") } } |
のようにします。
これならUIと課金処理を分離できます。
12. 今回のアプリなら、この構成がおすすめ
今回作られている「テーブル状のメモアプリ」のようなアプリなら、私は次の構成にします。
Google Play Console
│
│
premium_access
│
▼
┌─────────────────────────────────────┐
│ Android App │
│ │
│ Jetpack Compose │
│ │ │
│ ▼ │
│ PremiumViewModel │
│ │ │
│ ▼ │
│ BillingManager │
│ │ │
│ ▼ │
│ Google Play Billing Library │
│ │
└─────────────────────────────────────┘
そしてアプリ起動時には、
BillingClient接続
↓
購入済み商品を確認
↓
premium_accessを所有?
↙ ↘
YES NO
↓ ↓
機能解放 通常状態
購入時は、
「プレミアム機能を購入」
↓
Google Play購入画面
↓
購入成功
↓
PURCHASED確認
↓
プレミアム機能解放
↓
Acknowledge
という流れです。
AppleのStoreKit 2との大きな違い
以前実装されたiOS版と比較すると、かなり似ています。
| iOS / StoreKit 2 | Android / Play Billing |
|---|---|
| App Store Connect | Google Play Console |
| Non-Consumable | One-time product / Non-consumable |
| Product ID | Product ID |
Product.products | queryProductDetails |
product.purchase() | launchBillingFlow() |
Transaction | Purchase |
| 購入済みTransaction | queryPurchasesAsync() |
currentEntitlements | 購入済み商品をQuery |
| StoreKit | Google Play Billing |
| StoreKit 2 | Billing Library |
ただしAndroidでは、購入後のAcknowledgeを忘れないというのが特に重要です。Google公式も、クライアントのみで実装する場合は acknowledgePurchase() を使用する方法を案内しています。
なお、Googleは可能なら安全なバックエンドで購入検証することを推奨しています。個人開発の買い切りアプリでまず動かす段階ならクライアント側のBilling Libraryだけでも実装できますが、販売規模が大きくなったらサーバー側検証を検討するのがよいです。
テストに必要な準備
前提、Google Play Consoleでお支払いプロファイルを設定してあること。
Android StudioはGoogleアカウントと同期する必要はなし。
1. アプリをビルドしてaabファイルを作成する。
2. Google Play Consoleでアプリの登録をする。(アプリのIDとパッケージ名を合わせる)
3. 左のボードから【Google Playで収益化する】➡︎【商品】➡︎【1回限りのアイテム】で商品を登録する
ローカライズは赤枠内の色がついている角丸のボタンから該当言語を追加してプルダウンから選択
入力が終わったら左下の【次へ】のボタンで購入オプション(オプションとか書いてるが入力必須項目)でオプションID、金額を設定する。(このオプションIDは特別オファーとか設定して使う時に使うらしい)
金額設定は赤枠をクリック後【国 / 地域】の横に出現するチェックボックスをクリックした後、右下の【次へ】から一括入力することができる。
4. 【アイテムID】をコードに追加する。(【オプションID】ではない)
5. 左のボードから【テストとリリース】➡︎【テスト】➡︎【内部テスト】
【テスター数】タブで自分のGoogleアカウントのメールアドレスを追加する。
Google Play Consoleのホームに戻り【設定】➡︎【ライセンステスト】➡︎【ライセンステスター】に【テスター数】で登録したメーリングリストを登録する。
【リリース】タブでaabファイルをアップロードしリリースする。
実機でテストする場合はこのページに専用リンクがあるのでそれを使う。
はい、初期設定の RESPOND_NORMALLY のままで大丈夫です。 👍
今回の目的が、
非消費型の買い切り商品を、実際のGoogle Play Billingの流れでテストしたい
ということであれば、まずは RESPOND_NORMALLY を選んでおくのが適切です。
Google公式の説明でも、この項目はライセンスチェックに対して通常の処理をするか、特定のレスポンスを強制するかをテストするための設定です。(Android Developers)
RESPOND_NORMALLY の意味
これは、
「通常のGoogle Playのライセンス判定を返してください」
という設定です。
つまり、わざと
LICENSEDNOT_LICENSEDERROR_CONTACTING_SERVER
などの異常・特殊なレスポンスを返させるための設定ではありません。
通常の購入テストなら RESPOND_NORMALLY のままでOK です。
また、Googleのライセンステストでは、登録したテスターは実際の課金を発生させずにアプリ内課金をテストできます。(Google ヘルプ)
今回の設定なら
例えば、
設定
↓
ライセンステスト
↓
テスターのGoogleアカウントを登録
↓
ライセンス応答
RESPOND_NORMALLY ← ここはそのまま
↓
保存
で大丈夫です。
なお、この「ライセンス応答」は、今回実装しているGoogle Play Billingの購入処理そのものを成功/失敗させる設定ではありません。
古いGoogle Playの「アプリライセンスチェック(LVL)」関連のテスト機能です。今回の BillingClient によるアプリ内購入テストでは、まず ライセンステスターとしてGoogleアカウントを登録しておくことのほうが重要です。Google公式も、ライセンステスト機能をアプリ内課金のテストに利用できると案内しています。
なので、今の段階では RESPOND_NORMALLY → 変更なしでOK と考えて進めてください。👌
上記が終わったらテスト準備完了。Android StudioのGoogle Playのマークがついたシミュレータか実機で【テスター数】で登録したアカウントでGoogle Playにログイン後購入テストができる。
購入状態の解除はGoogle Play Consoleのホームの左【注文管理】から取り消せる【利用資格を削除する】にチェックを入れること。




