1. บทนำ
ระบบการเรียกเก็บเงินของ Google Play เป็นบริการที่ให้คุณขายผลิตภัณฑ์และเนื้อหาดิจิทัลในแอป Android ซึ่งเป็นวิธีโดยตรงที่สุดสำหรับคุณในการขายไอเทมที่ซื้อในแอปเพื่อสร้างรายได้จากแอป Codelab นี้จะแสดงวิธีใช้ Google Play Billing Library เพื่อขายการสมัครใช้บริการในโปรเจ็กต์ในลักษณะที่สรุปรายละเอียดสำคัญเมื่อผสานรวมการซื้อกับแอปอื่นๆ
นอกจากนี้ ยังแนะนำแนวคิดที่เกี่ยวข้องกับการสมัครใช้บริการ เช่น แพ็กเกจเริ่มต้น ข้อเสนอ แท็ก และแพ็กเกจแบบชำระเงินล่วงหน้า ดูข้อมูลเพิ่มเติมเกี่ยวกับการสมัครใช้บริการใน Google Play Billing ได้ที่ศูนย์ช่วยเหลือ
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ คุณจะต้องเพิ่มไลบรารีการเรียกเก็บเงินล่าสุด (เวอร์ชัน 5.0.0) ลงในแอปโปรไฟล์แบบสมัครใช้บริการที่ไม่ซับซ้อน แอปนี้สร้างขึ้นสำหรับคุณแล้ว คุณเพียงแค่เพิ่มส่วนการเรียกเก็บเงิน ดังที่แสดงในรูปที่ 1 ในแอปนี้ ผู้ใช้ลงชื่อสมัครใช้แพ็กเกจเริ่มต้นและ/หรือข้อเสนอใดๆ ที่เสนอผ่านผลิตภัณฑ์ที่ต้องสมัครใช้บริการแบบต่ออายุได้ 2 รายการ (แบบพื้นฐานและพรีเมียม) หรือแบบชำระเงินล่วงหน้าที่ต่ออายุไม่ได้ ไม่มีแล้ว แพ็กเกจเริ่มต้นเป็นการสมัครใช้บริการรายเดือนและรายปีตามลำดับ ผู้ใช้สามารถอัปเกรด ดาวน์เกรด หรือแปลงการสมัครใช้บริการแบบชำระล่วงหน้าเป็นการสมัครใช้บริการแบบต่ออายุได้
หากต้องการรวม Google Play Billing Library ไว้ในแอป คุณจะต้องสร้างสิ่งต่อไปนี้
BillingClientWrapper
- Wrapper สำหรับไลบรารี BillingClient เป้าหมายนี้มีจุดประสงค์เพื่อสรุปการโต้ตอบกับ BillingClient ของ Play Billing Library แต่ไม่จำเป็นต้องผสานรวมในการผสานรวมของคุณเองSubscriptionDataRepository
- ที่เก็บการเรียกเก็บเงินสำหรับแอปที่มีรายการสินค้าคงคลังของผลิตภัณฑ์ที่ต้องสมัครใช้บริการของแอป (เช่น ลดราคา) และรายการตัวแปร ShareFlow ที่ช่วยรวบรวมสถานะการซื้อและรายละเอียดผลิตภัณฑ์MainViewModel
- ViewModel ซึ่งใช้ติดต่อกับที่เก็บการเรียกเก็บเงินสำหรับส่วนที่เหลือของแอป การเปิดตัวขั้นตอนการเรียกเก็บเงินใน UI โดยใช้วิธีการซื้อแบบต่างๆ ช่วยได้
เมื่อเสร็จแล้ว สถาปัตยกรรมของแอปควรมีลักษณะคล้ายกับรูปด้านล่าง
สิ่งที่คุณจะได้เรียนรู้
- วิธีผสานรวม Play Billing Library
- วิธีสร้างผลิตภัณฑ์ที่ต้องสมัครใช้บริการ แพ็กเกจเริ่มต้น ข้อเสนอ และแท็กผ่าน Play Console
- วิธีดึงข้อมูลแพ็กเกจเริ่มต้นและข้อเสนอที่มีอยู่จากแอป
- วิธีเปิดใช้ขั้นตอนการเรียกเก็บเงินด้วยพารามิเตอร์ที่เหมาะสม
- วิธีเสนอผลิตภัณฑ์ที่ต้องสมัครใช้บริการแบบชำระล่วงหน้า
Codelab นี้มุ่งเน้นไปที่ Google Play Billing แนวคิดและโค้ดบล็อกที่ไม่เกี่ยวข้องจะปรากฎขึ้นและมีไว้เพื่อให้คุณคัดลอกและวางได้อย่างง่ายดาย
สิ่งที่คุณต้องมี
- Android Studio เวอร์ชันล่าสุด (>= Arctic Fox | 2020.3.1)
- อุปกรณ์ Android ที่ใช้ Android 8.0 ขึ้นไป
- โค้ดตัวอย่างที่พร้อมให้คุณใช้งานใน GitHub (ดูวิธีการในส่วนต่อไป)
- ความรู้เกี่ยวกับการพัฒนา Android ใน Android Studio ในระดับปานกลาง
- ความรู้เกี่ยวกับวิธีเผยแพร่แอปใน Google Play Store
- มีประสบการณ์ปานกลางในการเขียนโค้ด 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 Billing เริ่ม Android Studio และนำเข้า Billing-Codelab โดยเลือก Open > billing/PlayBillingCodelab
โครงการมี 2 โมดูลดังนี้
- start มีแอป Skeleton แต่ขาดทรัพยากร Dependency ที่จำเป็นและวิธีการทั้งหมดที่ต้องการใช้
- เสร็จสิ้น จะมีโปรเจ็กต์ที่ทำเสร็จแล้วและให้คำแนะนำได้ในกรณีที่คุณติดขัดได้
แอปประกอบด้วยไฟล์ของชั้นเรียน 8 ไฟล์ ได้แก่ BillingClientWrapper
, SubscriptionDataRepository
, Composables
, MainState
, MainViewModel
, MainViewModelFactory
และ MainActivity
- BillingClientWrapper คือ Wrapper ที่แยกเมธอด [BillingClient] ของ Google Play Billing ที่จำเป็นต่อการติดตั้งใช้งานอย่างง่ายและส่งคำตอบไปยังพื้นที่เก็บข้อมูลสำหรับการประมวลผล
- SubscriptionDataRepository ใช้ในการแยกแหล่งข้อมูลของ Google Play Billing (เช่น ไลบรารีของไคลเอ็นต์สำหรับการเรียกเก็บเงิน) และแปลงข้อมูล StateFlow ที่ปล่อยใน BillingClientWrapper เป็นโฟลว์
- ButtonModel คือคลาสข้อมูลที่ใช้ในการสร้างปุ่มใน UI
- Composables จะดึงเมธอด Composable ทั้งหมดของ UI ออกมาเป็น 1 คลาส
- MainState คือคลาสข้อมูลสำหรับการจัดการสถานะ
- MainViewModel ใช้เพื่อเก็บรักษาข้อมูลและสถานะที่เกี่ยวข้องกับการเรียกเก็บเงินที่ใช้ใน UI โดยจะรวมขั้นตอนทั้งหมดใน SubscriptionDataRepository เป็นออบเจ็กต์สถานะเดียว
- MainActivity คือคลาสกิจกรรมหลักที่โหลด Composable สำหรับอินเทอร์เฟซผู้ใช้
- ค่าคงที่คือออบเจ็กต์ที่มีค่าคงที่ที่หลายคลาสใช้
แกรเดิล
คุณต้องเพิ่มทรัพยากร Dependency แบบ Gradle เพื่อเพิ่ม Google Play Billing ลงในแอปของคุณ เปิดไฟล์build.gradle ของโมดูลแอป แล้วเพิ่มข้อมูลต่อไปนี้
dependencies { val billing_version = "5.0.0" implementation("com.android.billingclient:billing:$billing_version") }
คอนโซล Google Play
สำหรับวัตถุประสงค์ของ Codelab นี้ คุณต้องสร้างข้อเสนอผลิตภัณฑ์ที่ต้องสมัครใช้บริการ 2 รายการต่อไปนี้ในส่วนการสมัครใช้บริการของ Google Play Console
- การสมัครใช้บริการพื้นฐาน 1 รายการที่มีรหัสผลิตภัณฑ์
up_basic_sub
ผลิตภัณฑ์ควรมีแพ็กเกจเริ่มต้น 3 แบบ (ต่ออายุใหม่อัตโนมัติ 2 รายการและชำระล่วงหน้า 1 รายการ) ที่มีแท็กที่เกี่ยวข้อง ได้แก่ การสมัครใช้บริการรายเดือนพื้นฐาน 1 รายการที่มีแท็ก monthlybasic
การสมัครใช้บริการพื้นฐานรายปี 1 รายการที่มีแท็ก yearlybasic
และการสมัครใช้บริการแบบชำระเงินล่วงหน้า 1 รายการที่มีแท็ก prepaidbasic
คุณสามารถเพิ่มข้อเสนอไปยังแพ็กเกจเริ่มต้นได้ ข้อเสนอจะรับค่าแท็กจากแพ็กเกจเริ่มต้นที่เกี่ยวข้อง
- การสมัครใช้บริการพรีเมียม 1 รายการที่มีรหัสผลิตภัณฑ์
up_premium_sub
ผลิตภัณฑ์ควรมีแพ็กเกจเริ่มต้น 3 แบบ(ต่ออายุใหม่อัตโนมัติ 2 รายการและชำระล่วงหน้า 1 รายการ) ที่มีแท็กที่เกี่ยวข้อง ได้แก่ การสมัครใช้บริการรายเดือนพื้นฐาน 1 รายการที่มีแท็ก monthlypremium
การสมัครใช้บริการพื้นฐานรายปี 1 รายการที่มีแท็ก yearlypremium
และการสมัครใช้บริการแบบชำระเงินล่วงหน้า 1 รายการที่มีแท็ก prepaidpremium
คุณสามารถเพิ่มข้อเสนอไปยังแพ็กเกจเริ่มต้นได้ ข้อเสนอจะรับค่าแท็กจากแพ็กเกจเริ่มต้นที่เกี่ยวข้อง
สำหรับข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับวิธีสร้างผลิตภัณฑ์ที่ต้องสมัครใช้บริการ แพ็กเกจเริ่มต้น ข้อเสนอ และแท็ก โปรดดูศูนย์ช่วยเหลือของ Google Play
4. การตั้งค่าไคลเอ็นต์การเรียกเก็บเงิน
สำหรับส่วนนี้ คุณจะอยู่ในชั้นเรียนBillingClientWrapper
ในตอนท้าย คุณจะมีทุกอย่างที่จำเป็นสำหรับการสร้างอินสแตนซ์ไคลเอ็นต์การเรียกเก็บเงิน รวมทั้งวิธีการที่เกี่ยวข้องทั้งหมด
- เริ่มต้น BillingClient
เมื่อเราเพิ่มทรัพยากร Dependency ใน Google Play Billing Library แล้ว เราจำเป็นต้องเริ่มต้นอินสแตนซ์ BillingClient
BillingClientWrapper.kt
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
- สร้างการเชื่อมต่อกับ Google Play
หลังจากที่คุณสร้าง Billing Client แล้ว เราจำเป็นต้องเชื่อมต่อกับ 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)
}
})
}
- ค้นหา Google Play Billing สำหรับการซื้อที่มีอยู่
หลังจากที่เราสร้างการเชื่อมต่อกับ 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)
}
}
}
- แสดงผลิตภัณฑ์ที่พร้อมจำหน่าย
ตอนนี้เราสามารถค้นหาผลิตภัณฑ์ที่พร้อมจำหน่ายและแสดงให้ผู้ใช้เห็น ในการสอบถามรายละเอียดผลิตภัณฑ์ที่ต้องสมัครใช้บริการใน 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)
}
}
}
- ตั้งค่า Listener สำหรับการค้นหา ProductDetails
หมายเหตุ: วิธีนี้จะแสดงผลลัพธ์ของการค้นหาใน Maps ไปยัง _productWithProductDetails
และโปรดทราบว่าการค้นหาคาดว่าจะส่งคืน ProductDetails
หากไม่เกิดขึ้น ปัญหาน่าจะเกิดจากการที่ผลิตภัณฑ์ที่ตั้งค่าใน Play Console ยังไม่ได้เปิดใช้งาน หรือคุณไม่ได้เผยแพร่บิลด์ที่มีทรัพยากร Dependency ของไคลเอ็นต์การเรียกเก็บเงินในการติดตามการเผยแพร่รายการใดเลย
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")
}
}
}
- เปิดขั้นตอนการซื้อ
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)
}
- ตั้งค่า Listener สำหรับผลลัพธ์ของการดำเนินการซื้อ
เมื่อผู้ใช้ออกจากหน้าจอการซื้อของ Google Play (ไม่ว่าจะแตะปุ่ม "ซื้อ" เพื่อทำการซื้อให้เสร็จสมบูรณ์ หรือการแตะปุ่มย้อนกลับเพื่อยกเลิกการซื้อ) การโทรกลับของ onPurchaseUpdated()
จะส่งผลลัพธ์ของขั้นตอนการซื้อกลับไปยังแอปของคุณ จาก BillingResult.responseCode
คุณจะระบุได้ว่าผู้ใช้ซื้อผลิตภัณฑ์สำเร็จหรือไม่ หากเป็น responseCode == OK
หมายความว่าการซื้อเสร็จสมบูรณ์แล้ว
onPurchaseUpdated()
ส่งคืนรายการออบเจ็กต์ Purchase
รายการที่มีการซื้อทั้งหมดที่ผู้ใช้ดำเนินการผ่านแอป ออบเจ็กต์การซื้อแต่ละรายการจะมีแอตทริบิวต์รหัสผลิตภัณฑ์, 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.
}
}
- กำลังดำเนินการซื้อ (ยืนยันและตอบรับการซื้อ)
เมื่อผู้ใช้ทำการซื้อเสร็จสมบูรณ์ แอปจะต้องประมวลผลการซื้อนั้นโดยการตอบรับ
นอกจากนี้ ค่าของ _isNewPurchaseAcknowledged
จะตั้งค่าเป็น "จริง" เมื่อประมวลผลการรับทราบเรียบร้อยแล้ว
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
}
}
}
}
}
- สิ้นสุดการเชื่อมต่อการเรียกเก็บเงิน
ในขั้นสุดท้าย เมื่อกิจกรรมถูกทำลาย คุณต้องการยุติการเชื่อมต่อกับ Google Play เพื่อให้มีการเรียก endConnection()
เพื่อดำเนินการดังกล่าว
BillingClientWrapper.kt
fun terminateBillingConnection() {
Log.i(TAG, "Terminating connection")
billingClient.endConnection()
}
5. SubscriptionDataRepository
ใน BillingClientWrapper
คำตอบจาก QueryPurchasesAsync
และ QueryProductDetails
จะโพสต์ลงใน MutableStateFlow
_purchases
และ _productWithProductDetails
ตามลำดับ ซึ่งจะแสดงนอกชั้นเรียนพร้อมกับการซื้อและ productWithProductDetails
ในเดือนSubscriptionDataRepository
ระบบจะประมวลผลการซื้อเป็น 3 ขั้นตอนตามผลิตภัณฑ์ของการซื้อที่ส่งคืน ได้แก่ hasRenewableBasic
, hasPrepaidBasic
, hasRenewablePremium
และ hasPremiumPrepaid
นอกจากนี้ ระบบจะประมวลผล productWithProductDetails
เป็น basicProductDetails
และ premiumProductDetails
โฟลว์ที่เกี่ยวข้อง
6. โมเดลมุมมองหลัก
ส่วนที่ยากก็จบแล้ว ในขั้นตอนนี้ คุณจะกำหนด MainViewModel
ซึ่งเป็นเพียงอินเทอร์เฟซสาธารณะสำหรับลูกค้าของคุณ ลูกค้าจะได้ไม่ต้องทราบรายละเอียดภายในของ BillingClientWrapper
และ SubscriptionDataRepository
ก่อนอื่นใน 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 และค่าจะโพสต์ไปยังออบเจ็กต์ MutableLiveData ที่เรียกว่า _destinationScreen
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
ยังเพิ่มวิธีการบางอย่างที่มีประโยชน์ ดังนี้
- การดึงข้อมูลโทเค็นแพ็กเกจเริ่มต้นและข้อเสนอ
ตั้งแต่ 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
}
- การคำนวณข้อเสนอที่มีราคาต่ำสุด
เมื่อผู้ใช้มีสิทธิ์รับข้อเสนอหลายรายการ ระบบจะใช้เมธอด leastPricedOfferToken()
เพื่อคำนวณข้อเสนอที่ต่ำที่สุดจากข้อเสนอที่ retrieveEligibleOffers()
ส่งคืน
วิธีนี้จะแสดงโทเค็นรหัสข้อเสนอของข้อเสนอที่เลือก
การใช้งานนี้แสดงเพียงข้อเสนอที่มีราคาต่ำสุดในแง่ของชุด 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
}
- เครื่องมือสร้าง 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()
)
)
}
- วิธีการซื้อ
วิธีการซื้อใช้ launchBillingFlow()
ของ BillingClientWrapper
และ 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.")
}
}
- สิ้นสุดการเชื่อมต่อการเรียกเก็บเงิน
สุดท้าย ระบบจะเรียกใช้เมธอด terminateBillingConnection
ของ BillingClientWrapper
ใน onCleared()
ของ ViewModel
การดำเนินการนี้จะสิ้นสุดการเชื่อมต่อการเรียกเก็บเงินปัจจุบันเมื่อกิจกรรมที่เกี่ยวข้องถูกทำลาย
7. UI
ตอนนี้ก็ถึงเวลาใช้ทุกอย่างที่คุณสร้างใน UI เพื่อช่วยในการดำเนินการดังกล่าว คุณจะต้องทำงานร่วมกับชั้นเรียน Composables และ MainActivity
Composables.kt
คลาส Composables ถูกให้มาอย่างสมบูรณ์และกำหนดฟังก์ชันการเขียนทั้งหมดที่ใช้ในการแสดงผล UI และกลไกการนำทางระหว่างฟังก์ชันทั้งสอง
ฟังก์ชัน Subscriptions
แสดงปุ่ม 2 ปุ่ม ได้แก่ Basic Subscription
และ Premium Subscription
Basic Subscription
และ Premium Subscription
จะโหลดวิธีการ Compose ใหม่ซึ่งแสดงแพ็กเกจเริ่มต้นที่เกี่ยวข้อง 3 แบบ ได้แก่ รายเดือน รายปี และแบบชําระล่วงหน้า
จากนั้นจะมีฟังก์ชันการเขียนโปรไฟล์ที่เป็นไปได้ 3 ฟังก์ชันซึ่งแต่ละฟังก์ชันสำหรับการสมัครใช้บริการที่ผู้ใช้อาจมี ได้แก่ โปรไฟล์ Basic แบบต่ออายุได้ Premium แบบต่ออายุได้ และโปรไฟล์ Prepaid Basic หรือโปรไฟล์ Premium แบบชำระเงินล่วงหน้า
- เมื่อผู้ใช้สมัครใช้บริการขั้นพื้นฐาน โปรไฟล์พื้นฐานจะอนุญาตให้ผู้ใช้อัปเกรดเป็นการสมัครใช้บริการแบบพรีเมียม รายเดือน หรือแบบชำระล่วงหน้าได้
- ในทางกลับกัน เมื่อผู้ใช้มีการสมัครใช้บริการแบบพรีเมียม ก็จะดาวน์เกรดเป็นการสมัครใช้บริการรายเดือน รายปี หรือแบบชำระล่วงหน้าได้
- เมื่อผู้ใช้มีการสมัครใช้บริการแบบชำระเงินล่วงหน้า ผู้ใช้จะเติมเงินค่าสมัครใช้บริการด้วยปุ่มเติมเงินหรือแปลงการสมัครใช้บริการแบบชำระล่วงหน้าเป็นแพ็กเกจเริ่มต้นแบบต่ออายุใหม่อัตโนมัติที่เกี่ยวข้องได้
สุดท้าย จะมีฟังก์ชันหน้าจอการโหลดที่ใช้เมื่อเชื่อมต่อกับ Google Play และเมื่อมีการแสดงผลโปรไฟล์ผู้ใช้
MainActivity.kt
- เมื่อสร้าง
MainActivity
ระบบจะสร้างอินสแตนซ์ viewModel และโหลดฟังก์ชันเขียนที่ชื่อMainNavHost
MainNavHost
เริ่มต้นด้วยตัวแปรisBillingConnected
ที่สร้างจากbillingConnectionSate
Livedata ของ viewModel และสังเกตการเปลี่ยนแปลงเนื่องจากเมื่อมีการสร้างอินสแตนซ์ vieModel ระบบจะส่งbillingConnectionSate
ไปยังเมธอด startBillingConnection ของBillingClientWrapper
isBillingConnected
จะตั้งค่าเป็น "จริง" เมื่อสร้างการเชื่อมต่อแล้ว และจะตั้งค่าเป็น "เท็จ" หากไม่มี
เมื่อเป็นเท็จ ฟังก์ชันเขียน LoadingScreen()
จะโหลดขึ้น และเมื่อเป็นจริง Subscription
หรือฟังก์ชันโปรไฟล์จะโหลดขึ้น
val isBillingConnected by viewModel.billingConnectionState.observeAsState()
- เมื่อสร้างการเชื่อมต่อการเรียกเก็บเงินแล้ว
มีการสร้างอินสแตนซ์ navController
val navController = rememberNavController()
จากนั้นระบบจะรวบรวมขั้นตอนใน MainViewModel
val productsForSale by viewModel.productsForSaleFlows.collectAsState(
initial = MainState()
)
val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
initial = listOf()
)
ขั้นสุดท้ายจะสังเกตตัวแปร LiveData destinationScreen
ของ viewModel
ระบบจะแสดงฟังก์ชันการเขียนที่เกี่ยวข้องตามสถานะการสมัครใช้บริการปัจจุบันของผู้ใช้
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 ไว้ในแอปที่เรียบง่ายมากๆ เรียบร้อยแล้ว
โปรดดูตัวอย่างอย่างเป็นทางการสำหรับแอปที่ซับซ้อนมากขึ้นในเวอร์ชันที่ระบุไว้พร้อมการตั้งค่าเซิร์ฟเวอร์ที่ปลอดภัย