我們發佈 APP 有很大的一份部就是為了賺錢吧
那麼就不可不知,如何實作 APP 內購功能
無論是一次性的遊戲藥水,還是週期性的訂閱服務
用 2019 Google IO 推出的 Google Play Billing Library 一次做給你看
在開始之前我們有一些程式外的資訊需要設定
1、取得開發者帳號
2、在 Google Play Console 設定付款資訊 (金流用的)
3、把 1 & 2 關聯起來
https://developer.android.com/google/play/billing/getting-ready#account
4、在 Google Play Console 建立商品 (需先上傳有權限 apk)
https://support.google.com/googleplay/android-developer/answer/1153481
以上跟著 Google 的引導做,應該就可以了
完成設定且建立好商品的人應該可以在 Google Play Console 看到以下畫面
再來就是 APP 這裡要開始實作 Library 了
首先要有內購的權限,在 manifast 加入下列
<!-- 內購-->
<uses-permission android:name="com.android.vending.BILLING" />
在 app/gradle dependencies 加入依賴
// APP 內購,Google Play In-App billing.
implementation 'com.android.billingclient:billing-ktx:3.0.0'
rebuild 之後,我們來寫一個 lifecycle observer
用來管理 BillingClient 的生命週期
class BillingClientLifecycle private constructor(
private val app: Application
) : LifecycleObserver,
PurchasesUpdatedListener,
BillingClientStateListener {
private lateinit var billingClient: BillingClient
companion object {
@Volatile
private var INSTANCE: BillingClientLifecycle? = null
fun getInstance(app: Application): BillingClientLifecycle =
INSTANCE ?: synchronized(this) {
INSTANCE ?: BillingClientLifecycle(app)
.also { INSTANCE = it }
}
}
可以看到我們用 singleton 來生成這個 class
並 impletment
LifecycleObserver, PurchasesUpdatedListener, BillingClientStateListener
然後 LifecycleObserver 這邊的 onCreate 實作如下
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun create() {
PLog.d("$TAG: ON_CREATE")
billingClient = BillingClient
.newBuilder(app.applicationContext)
.setListener(this)
.enablePendingPurchases() // Not used for subscriptions.
.build()
if (!billingClient.isReady) {
PLog.d("$TAG: BillingClient: Start connection...")
billingClient.startConnection(this)
}
}
在 onCreate 我們做了連線至 Billing Library 的動作
所以相對的,我們也會在 onDestory 進行 disconnection
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
PLog.d("$TAG: ON_DESTROY")
if (billingClient.isReady) {
PLog.d("$TAG: closing connection")
// After calling endConnection()
// we must create a new BillingClient.
billingClient.endConnection()
}
}
這樣就控制住了連線與斷線
再來我們就可以利用 BillingClientStateListener 做事情
比如說:連線後查詢 or 斷線後重連
/** === BillingClientStateListener Start === */
override fun onBillingSetupFinished(
billingResult: BillingResult) {
val responseCode = billingResult.responseCode
val debugMessage = billingResult.debugMessage
PLog.d("$TAG:
onBillingSetupFinished: $responseCode $debugMessage")
if (responseCode == BillingClient.BillingResponseCode.OK) {
// The billing client is ready.
GlobalScope.launch(Dispatchers.IO) {
try {
querySkuDetails()
queryPurchases()
} catch (e: Throwable) {
PLog.e(e, "$TAG query:")
} finally {
this.cancel()
}
}
}
}
override fun onBillingServiceDisconnected() {
PLog.d("$TAG: onBillingServiceDisconnected")
// Try connecting again with exponential backoff.
billingClient.startConnection(this)
}
/** === BillingClientStateListener End === */
一切都設設定好之後
就來看看怎麼 query 吧
suspend fun querySkuDetails() {
PLog.d("$TAG: querySkuDetails")
val params = SkuDetailsParams.newBuilder()
.setType(BillingClient.SkuType.SUBS)
.setSkusList(
listOf(SKU.Test001.id, SKU.Test002.id)
).build()
val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(params)
}
val billingResult = skuDetailsResult.billingResult
val responseCode = billingResult.responseCode
val debugMessage = billingResult.debugMessage
PLog.d("$TAG:
onSkuDetailsResponse: $responseCode $debugMessage")
PLog.obj(skuDetailsResult)
when (responseCode) {
BillingClient.BillingResponseCode.OK -> {
skuDetailsResult.skuDetailsList?.also {
skusWithSkuDetails.postValue(
HashMap<String, SkuDetails>().apply {
it.forEach {
put(it.sku, it)
}
}.also {
PLog.d("$TAG: onSkuDetailsResponse:
count ${it.size}")
}
)
} ?: also {
PLog.d("$TAG: onSkuDetailsResponse:
null SkuDetails list")
skusWithSkuDetails.postValue(emptyMap())
}
}
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE,
BillingClient.BillingResponseCode.ITEM_UNAVAILABLE,
BillingClient.BillingResponseCode.DEVELOPER_ERROR,
BillingClient.BillingResponseCode.ERROR -> {
PLog.e("$TAG: onSkuDetailsResponse:
$responseCode $debugMessage")
}
BillingClient.BillingResponseCode.USER_CANCELED,
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED,
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED,
BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
// These response codes are not expected.
PLog.d("$TAG: onSkuDetailsResponse:
$responseCode $debugMessage")
}
}
}
附註:
SKU 是我用來記錄商品 ID 的一個 enum class
SkuTpye 有兩種 IN_APP & SUBS
skusWithSkuDetails 是用來通知更新 UI 的 LiveData
至此,我們完成了第一階段取得商品的部份
再來就可以進入下一階段,啟動購買流程囉!
若有遇到商品列表為空的朋友們
可以看這一篇
資料來源:
https://developer.android.com/google/play/billing
https://github.com/android/play-billing-samples
留言列表