entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Google Play Billing Library v3に対応した話

みなさんはじめまして。初めてじゃない方はお久しぶりです。pixivコミックAndroidアプリ担当のconsommeです。好きなウマ娘はトウカイテイオーマヤノトップガンです。

Androidアプリにおいて、アプリ内で使えるアイテムやサービスの有料会員登録などを行う上で必要になるのが Google Play Billing Library(以下PBL)です。こちらの最新版が2021年3月時点でバージョン3なのですが、2021年8月以降は新規アプリで、同年11月以降はすべてのアプリにおいてバージョン3の使用が必須になります。そう、既存のアプリでも猶予はあと半年ほどしかないのです。 android-developers-jp.googleblog.com

とは言え、アプリ内購入周りはお金が絡む部分のため、対応するにも慎重にならざるを得ません。もし不具合が発生したら…というのを考えると、なかなか手を付けられないかと思います。しかし、前述したとおりもうデッドラインが迫ってきています。嫌でも対応しなければなりません。

今回は、pixivコミックアプリでPBLバージョン3(以下PBLv3)の実装を行った際に工夫した点などをご紹介します。 PBLの概要や基本的な実装方法についてはここでは紹介しませんので、公式のドキュメントやサンプルコードを参照してください。 developer.android.com github.com

また、DroidKaigi 2020でsyarihuさんが発表された内容が非常に参考になりますので、あわせてご覧ください。
www.youtube.com

アプリ内購入用モジュールを作る

pixivコミックアプリは現状appモジュールのみのモノリシック構成となっています。しかし、PBLの実装についてappモジュールに依存しない設計ができると考えたため、別モジュールで設計・実装しました。これは、使用する際はPBLモジュールのrepositoryを介せばよいため、非常にわかりやすい構成となります。

簡単に各クラスの説明です。まずManagerクラスは、PBLにおいてGoogle Play Servicesへの接続など重要な機能を持つBillingClientクラスを管理するクラスです。購入画面の起動や購入履歴の取得など、repositoryから呼び出すためのメソッドを実装します。

Repositoryクラスでは、appモジュール(ViewModelやUseCaseなど)から呼び出すためのメソッドを実装します。今回はベースになるabstract classを用意し、1回限りのアイテム用のrepositoryと定期購入用のrepositoryを実装しました。

abstract class BillingRepository {

    internal abstract val billingManager: BillingManager

    // 未消費のPurchaseを取得する
    abstract fun queryPurchases(): List<Purchase>

    // 購入画面を起動する
    abstract suspend fun launchPurchase(activity: Activity, sku: String)
    ...
}

1回限りの購入用と定期購入用の実装を入れたものがこちらです。BillingRepositoryのメソッドをオーバーライドして、それぞれManagerのメソッドを呼び出すように実装します。Daggerを使ってBillingManagerをinjectしていますが、こちらの理由については後述します。

class InAppBillingRepository @Inject constructor(override val billingManager: BillingManager) : BillingRepository() {

    override fun queryPurchases(): List<Purchase> {
        return billingManager.getUnconsumedPurchasesInApp()
    }

    override suspend fun launchPurchase(activity: Activity, sku: String) {
        billingManager.launchBillingInApp(activity, sku)
    }
}
class SubsBillingRepository @Inject constructor(override val billingManager: BillingManager) : BillingRepository() {

    override fun queryPurchases(): List<Purchase> {
        return billingManager.getUnconsumedPurchasesSubscription()
    }

    override suspend fun launchPurchase(activity: Activity, sku: String) {
        billingManager.launchBillingSubscription(activity, sku)
    }
}

LifecycleObserverでBillingClientの接続状態を管理する

BillingClientは使う前にstartConnectionメソッドを叩く必要がありますが、毎回明示的に呼び出すのは面倒です。そこで、BillingManagerにLifecycleObserverを実装し、ON_CREATEON_DESTROYで接続および終了処理を行います。これにより明示的にstartConnectionしなくても、親のLifecycleに合わせて自動で処理してくれるため実装が簡単になります。

private lateinit var billingClient: BillingClient

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
    billingClient = BillingClient.newBuilder(context)
        .setListener(purchasesUpdatedListener)
        .enablePendingPurchases()
        .build()
        .startConnection(billingClientStateListener)
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
    billingClient.endConnection()
}

デバッグビルド用のManagerを作る

PBLのメソッドを実行する際、Play Consoleにアップロードされているビルドと同じ署名・同じパッケージ名でないと正常に動作しません(ライセンステストに登録されているアカウントで動かす場合は例外)。つまりデバッグ用の署名だと動いてくれません。その結果、Android Studioでブレークポイントを設定して変数の状態や動きを確認すると言ったことができなくなり、開発する際に非常に不便です。 そこで、デバッグビルド用にManagerクラスを作り、そちらではBillingClientのメソッドを使用する代わりにモックデータを返すようにします。

src/mainとは別にsrc/debugディレクトリを作成し、mainと同じディレクトリ構成、同じクラス名でModuleクラスを配置し、Managerクラスのインスタンスを生成します。そうするとデバッグビルドでは自動的にsrc/debug以下の方が参照されるため、デバッグ用のManagerが使われる仕組みです。

├── debug
│   └── java
│       └── jp.pxv.android.manga.billing
│           ├── DebugBillingManager.kt (class)
│           └── di
│               └── BillingModule.kt
├── main
│   └── java
│       └── jp.pxv.android.manga.billing
│           ├── BillingManager.kt (Interface)
│           └── DefaultBillingManager.kt (class)
└── release
    └── java
        └── jp.pxv.android.manga.billing
            └── di
                └── BillingModule.kt

下記の例では、購入画面を開くメソッドを実行した際にAlertDialogを表示し、実行時に任意のレスポンスを受け取れるようにしています。

class DebugBillingManager : BillingManager() {

    override suspend fun launchBillingInApp(activity: Activity, sku: String) {
        showResponseDialog(activity, sku)
    }

    override suspend fun launchBillingSubscription(activity: Activity, sku: String) {
        showResponseDialog(activity, sku)
    }

    private fun showResponseDialog(context: Context, sku: String) {
        AlertDialog.Builder(context)
            .setTitle("${sku}のレスポンス")
            .setItems(responses.map { context.getString(Response(it).getMessageRes()) }
                .toTypedArray()) { _, which ->
                run {
                    _billingResponseFlow.tryEmit(Response(responses[which]))
                }
            }
            .show()
    }
}

実際にデバッグビルドで実行するとこんな感じです。コイン購入ボタンを押すと、Google Playの購入画面の代わりにこういうダイアログが表示され、選んだレスポンスが返却されます。あとは受け取った値をもとにapp側で処理を行えばOKです。

KTXモジュールを使う(Kotlin実装の場合)

公式のドキュメントでも紹介されていますが、PBLにはKTXバージョンが用意されています。こちらにはコルーチンのサポートが含まれており、通常のPBLではListener経由で処理を行ういくつかの関数がsuspend関数になっているため、コルーチンで書きやすくなっています。 例として、アプリ内アイテムの情報を取得するquerySkuDetailsのメソッドを使う際の実装コードを見てみましょう。

KTXを使わない場合

billingClient.querySkuDetailsAsync(skuDetailsParams) { billingResult, mutableList ->
    val billingFlowParams = mutableList?.first()?.let {
        BillingFlowParams.newBuilder()
            .setSkuDetails(it)
            .build()
    }
    ….
}

KTXを使う場合(suspend関数内で使用)

val billingFlowParams =
    billingClient.querySkuDetails(skuDetailsParams).skuDetailsList?.first()?.let {
        BillingFlowParams.newBuilder()
            .setSkuDetails(it)
            .build()
    }
...

同じ処理内容でも、Listenerの処理がなくなった分、インデントが一つ消えてコードがスッキリと読みやすくなりました。コルーチンを使える場所は積極的に使っていきましょう。

まとめ

今回、おそらく実装されてから初めてアプリ内購入まわりの実装に手を加えることになりました。pixivコミックアプリではAIDLを使った実装を使い続けていたため、ほぼ機能をまるごと新規で作ることになりましたが、逆に古い実装を一掃できたのでよかったかなと思います。モジュールを分けて実装したのも今後のマルチモジュール実装を見据えてのことで、よい練習になったかなと思います。

ただし、PBLについては今後一年周期で新バージョンがリリースされることがGoogleから予告されています。2021年中にPBLv4がリリースされ、v3は2022年中にサポート終了となると思われます。つまり今年中(遅くとも来年中旬ごろまで)にv4に対応する必要があります。 今後はアプリ内購入については「一度作ったらもう触らない」ということはなくなりますので、メンテナンスやテストがしやすいような実装を心がけていきましょう。

最後になりますが、ピクシブ株式会社では新卒・中途ともにAndroidアプリエンジニアを募集しています。pixivコミック以外にもたくさんのサービスがあります。技術の力で創作活動を支援したい方、お待ちしています!

hrmos.co

hrmos.co

consomme
2012年10月入社。入社以来ずっとAndroidアプリ担当(pixiv→BOOTH→pixivコミック)。旅行と写真と麻雀が趣味。