ขายการสมัครใช้บริการในแอปด้วย Play Billing Library 5

1. บทนำ

ระบบการเรียกเก็บเงินของ Google Play เป็นบริการที่ให้คุณขายผลิตภัณฑ์และเนื้อหาดิจิทัลในแอป Android ซึ่งเป็นวิธีโดยตรงที่สุดสำหรับคุณในการขายไอเทมที่ซื้อในแอปเพื่อสร้างรายได้จากแอป Codelab นี้จะแสดงวิธีใช้ Google Play Billing Library เพื่อขายการสมัครใช้บริการในโปรเจ็กต์ในลักษณะที่สรุปรายละเอียดสำคัญเมื่อผสานรวมการซื้อกับแอปอื่นๆ

นอกจากนี้ ยังแนะนำแนวคิดที่เกี่ยวข้องกับการสมัครใช้บริการ เช่น แพ็กเกจเริ่มต้น ข้อเสนอ แท็ก และแพ็กเกจแบบชำระเงินล่วงหน้า ดูข้อมูลเพิ่มเติมเกี่ยวกับการสมัครใช้บริการใน Google Play Billing ได้ที่ศูนย์ช่วยเหลือ

สิ่งที่คุณจะสร้าง

ใน Codelab นี้ คุณจะต้องเพิ่มไลบรารีการเรียกเก็บเงินล่าสุด (เวอร์ชัน 5.0.0) ลงในแอปโปรไฟล์แบบสมัครใช้บริการที่ไม่ซับซ้อน แอปนี้สร้างขึ้นสำหรับคุณแล้ว คุณเพียงแค่เพิ่มส่วนการเรียกเก็บเงิน ดังที่แสดงในรูปที่ 1 ในแอปนี้ ผู้ใช้ลงชื่อสมัครใช้แพ็กเกจเริ่มต้นและ/หรือข้อเสนอใดๆ ที่เสนอผ่านผลิตภัณฑ์ที่ต้องสมัครใช้บริการแบบต่ออายุได้ 2 รายการ (แบบพื้นฐานและพรีเมียม) หรือแบบชำระเงินล่วงหน้าที่ต่ออายุไม่ได้ ไม่มีแล้ว แพ็กเกจเริ่มต้นเป็นการสมัครใช้บริการรายเดือนและรายปีตามลำดับ ผู้ใช้สามารถอัปเกรด ดาวน์เกรด หรือแปลงการสมัครใช้บริการแบบชำระล่วงหน้าเป็นการสมัครใช้บริการแบบต่ออายุได้

d7dba51f800a6cc4.png 2220c15b849d2ead.png

หากต้องการรวม Google Play Billing Library ไว้ในแอป คุณจะต้องสร้างสิ่งต่อไปนี้

  • BillingClientWrapper- Wrapper สำหรับไลบรารี BillingClient เป้าหมายนี้มีจุดประสงค์เพื่อสรุปการโต้ตอบกับ BillingClient ของ Play Billing Library แต่ไม่จำเป็นต้องผสานรวมในการผสานรวมของคุณเอง
  • SubscriptionDataRepository - ที่เก็บการเรียกเก็บเงินสำหรับแอปที่มีรายการสินค้าคงคลังของผลิตภัณฑ์ที่ต้องสมัครใช้บริการของแอป (เช่น ลดราคา) และรายการตัวแปร ShareFlow ที่ช่วยรวบรวมสถานะการซื้อและรายละเอียดผลิตภัณฑ์
  • MainViewModel - ViewModel ซึ่งใช้ติดต่อกับที่เก็บการเรียกเก็บเงินสำหรับส่วนที่เหลือของแอป การเปิดตัวขั้นตอนการเรียกเก็บเงินใน UI โดยใช้วิธีการซื้อแบบต่างๆ ช่วยได้

เมื่อเสร็จแล้ว สถาปัตยกรรมของแอปควรมีลักษณะคล้ายกับรูปด้านล่าง

c83bc759f32b0a63.png

สิ่งที่คุณจะได้เรียนรู้

  • วิธีผสานรวม 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

a9f6fd6e70e69fed.png

คุณสามารถเพิ่มข้อเสนอไปยังแพ็กเกจเริ่มต้นได้ ข้อเสนอจะรับค่าแท็กจากแพ็กเกจเริ่มต้นที่เกี่ยวข้อง

สำหรับข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับวิธีสร้างผลิตภัณฑ์ที่ต้องสมัครใช้บริการ แพ็กเกจเริ่มต้น ข้อเสนอ และแท็ก โปรดดูศูนย์ช่วยเหลือของ Google Play

4. การตั้งค่าไคลเอ็นต์การเรียกเก็บเงิน

สำหรับส่วนนี้ คุณจะอยู่ในชั้นเรียนBillingClientWrapper

ในตอนท้าย คุณจะมีทุกอย่างที่จำเป็นสำหรับการสร้างอินสแตนซ์ไคลเอ็นต์การเรียกเก็บเงิน รวมทั้งวิธีการที่เกี่ยวข้องทั้งหมด

  1. เริ่มต้น BillingClient

เมื่อเราเพิ่มทรัพยากร Dependency ใน Google Play Billing Library แล้ว เราจำเป็นต้องเริ่มต้นอินสแตนซ์ BillingClient

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. สร้างการเชื่อมต่อกับ 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)
       }
   })
}
  1. ค้นหา 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)
       }
   }
}
  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. ตั้งค่า 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")
       }
   }
}
  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. ตั้งค่า 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.
   }
}
  1. กำลังดำเนินการซื้อ (ยืนยันและตอบรับการซื้อ)

เมื่อผู้ใช้ทำการซื้อเสร็จสมบูรณ์ แอปจะต้องประมวลผลการซื้อนั้นโดยการตอบรับ

นอกจากนี้ ค่าของ _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
               }
           }
       }
   }
}
  1. สิ้นสุดการเชื่อมต่อการเรียกเก็บเงิน

ในขั้นสุดท้าย เมื่อกิจกรรมถูกทำลาย คุณต้องการยุติการเชื่อมต่อกับ 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 ยังเพิ่มวิธีการบางอย่างที่มีประโยชน์ ดังนี้

  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() ส่งคืน

วิธีนี้จะแสดงโทเค็นรหัสข้อเสนอของข้อเสนอที่เลือก

การใช้งานนี้แสดงเพียงข้อเสนอที่มีราคาต่ำสุดในแง่ของชุด 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

เมื่อต้องการเปิดตัวขั้นตอนการซื้อ สำหรับผลิตภัณฑ์หนึ่งๆ ผลิตภัณฑ์นั้น" ต้องตั้งค่า 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()
       )
   )
}
  1. วิธีการซื้อ

วิธีการซื้อใช้ 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.")
   }
}

d726a27add092140.png

  1. สิ้นสุดการเชื่อมต่อการเรียกเก็บเงิน

สุดท้าย ระบบจะเรียกใช้เมธอด 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

  1. เมื่อสร้าง MainActivity ระบบจะสร้างอินสแตนซ์ viewModel และโหลดฟังก์ชันเขียนที่ชื่อ MainNavHost
  2. MainNavHost เริ่มต้นด้วยตัวแปร isBillingConnected ที่สร้างจาก billingConnectionSate Livedata ของ viewModel และสังเกตการเปลี่ยนแปลงเนื่องจากเมื่อมีการสร้างอินสแตนซ์ vieModel ระบบจะส่ง billingConnectionSate ไปยังเมธอด startBillingConnection ของ BillingClientWrapper

isBillingConnected จะตั้งค่าเป็น "จริง" เมื่อสร้างการเชื่อมต่อแล้ว และจะตั้งค่าเป็น "เท็จ" หากไม่มี

เมื่อเป็นเท็จ ฟังก์ชันเขียน LoadingScreen() จะโหลดขึ้น และเมื่อเป็นจริง Subscription หรือฟังก์ชันโปรไฟล์จะโหลดขึ้น

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. เมื่อสร้างการเชื่อมต่อการเรียกเก็บเงินแล้ว

มีการสร้างอินสแตนซ์ 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 ไว้ในแอปที่เรียบง่ายมากๆ เรียบร้อยแล้ว

โปรดดูตัวอย่างอย่างเป็นทางการสำหรับแอปที่ซับซ้อนมากขึ้นในเวอร์ชันที่ระบุไว้พร้อมการตั้งค่าเซิร์ฟเวอร์ที่ปลอดภัย

อ่านเพิ่มเติม