Menjual langganan di aplikasi dengan Library Layanan Penagihan Play 5

1. Pengantar

Sistem penagihan Google Play adalah layanan yang memungkinkan Anda menjual produk dan konten digital di aplikasi Android. Ini adalah cara paling langsung bagi Anda untuk menjual produk dalam aplikasi untuk memonetisasi aplikasi Anda. Codelab ini menunjukkan cara menggunakan Library Layanan Penagihan Google Play untuk menjual langganan dalam project Anda dengan cara yang merangkum detail mendetail saat mengintegrasikan pembelian dengan bagian aplikasi lainnya.

Panduan ini juga memperkenalkan konsep terkait langganan seperti paket dasar, penawaran, tag, dan paket prabayar. Untuk mempelajari langganan di Layanan Penagihan Google Play lebih lanjut, Anda dapat membuka Pusat Bantuan kami.

Yang akan Anda build

Dalam codelab ini, Anda akan menambahkan library penagihan terbaru (versi 5.0.0) ke aplikasi profil berbasis langganan sederhana. Aplikasi ini sudah dibuat untuk Anda, jadi Anda hanya akan menambahkan bagian penagihan. Seperti yang ditampilkan dalam gambar 1, dalam aplikasi ini pengguna mendaftar ke paket dasar dan/atau penawaran apa pun yang ditawarkan melalui dua produk langganan yang dapat diperpanjang (dasar dan premium) atau untuk prabayar yang tidak dapat diperpanjang. Itu saja. Paket dasar masing-masing adalah langganan bulanan dan tahunan. Pengguna dapat mengupgrade, mendowngrade, atau mengonversi langganan prabayar ke langganan yang dapat diperpanjang.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Untuk memasukkan Library Layanan Penagihan Google Play di aplikasi, Anda harus membuat hal berikut:

  • BillingClientWrapper: wrapper untuk library BillingClient. Library ini bermaksud mengenkapsulasi interaksi dengan BillingClient Library Layanan Penagihan Play, tetapi tidak diperlukan dalam integrasi Anda sendiri.
  • SubscriptionDataRepository - repositori penagihan untuk aplikasi Anda yang berisi daftar inventaris produk langganan aplikasi (yaitu apa yang dijual), dan daftar variabel ShareFlow yang membantu mengumpulkan status pembelian dan detail produk
  • MainViewModel - ViewModel yang digunakan aplikasi Anda untuk berkomunikasi dengan repositori penagihan. Hal ini membantu meluncurkan alur penagihan di UI menggunakan berbagai metode pembelian.

Setelah selesai, arsitektur aplikasi Anda akan terlihat mirip dengan gambar di bawah ini:

c83bc759f32b0a63.png

Yang akan Anda pelajari

  • Cara mengintegrasikan library layanan penagihan Play
  • Cara membuat produk langganan, paket dasar, penawaran, dan tag melalui Konsol Play
  • Cara mengambil penawaran dasar dan penawaran yang tersedia dari aplikasi
  • Cara meluncurkan alur penagihan dengan parameter yang sesuai
  • Cara menawarkan produk langganan prabayar

Codelab ini berfokus pada Layanan Penagihan Google Play. Konsep dan blok kode yang tidak-relevan akan dipoles dan disediakan sehingga Anda cukup salin dan tempel.

Yang Anda butuhkan

  • Versi terbaru Android Studio (>= Arctic Fox | 2020.3.1)
  • Perangkat Android dengan Android 8.0 atau lebih baru
  • Kode contoh, yang disediakan untuk Anda di GitHub (petunjuk di bagian selanjutnya)
  • Pengetahuan sedang tentang pengembangan Android di Android Studio
  • Pengetahuan tentang cara memublikasikan aplikasi ke Google Play Store
  • Pengalaman menengah dalam menulis kode Kotlin

2. Mempersiapkan

Dapatkan kode dari GitHub

Kami telah memasukkan semua yang Anda perlukan untuk project ini ke dalam repo Git. Untuk memulai, Anda harus mengambil kode tersebut dan membukanya di lingkungan pengembangan favorit Anda. Untuk codelab ini, sebaiknya gunakan Android Studio.

Kode untuk memulai disimpan di repositori GitHub. Anda dapat meng-clone repositori melalui perintah berikut:

git clone https://github.com/android/play-billing-codelabs

3. Pekerjaan dasar

Apa yang menjadi titik awal?

Titik awal kita adalah aplikasi profil pengguna dasar yang dirancang untuk codelab ini. Kode telah disederhanakan untuk menunjukkan konsep yang ingin kita ilustrasikan dan belum dirancang untuk penggunaan produksi. Jika Anda memilih untuk menggunakan kembali bagian mana pun dari kode ini dalam aplikasi produksi, pastikan untuk mengikuti praktik terbaik dan sepenuhnya menguji semua kode Anda.

Mengimpor project ke Android Studio

Codelab penagihan adalah aplikasi dasar yang tidak berisi penerapan Layanan Penagihan Google Play. Mulai Android Studio dan impor codelab penagihan dengan memilih **Open > billing-codelab/build.gradle**

Project ini memiliki dua paket:

  • codelab memiliki aplikasi kerangka, tetapi tidak memiliki dependensi yang diperlukan dan semua metode yang perlu Anda terapkan
  • solution sudah selesai dan dapat berfungsi sebagai panduan saat Anda mengalami masalah

Aplikasi ini terdiri dari tujuh file class: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory, dan MainActivity.

  • BillingClientWrapper adalah wrapper yang mengisolasi metode [BillingClient] Layanan Penagihan Google Play yang diperlukan untuk memiliki implementasi sederhana dan memberikan respons ke repositori data untuk diproses.
  • SubscriptionDataRepository digunakan untuk memisahkan sumber data Layanan Penagihan Google Play (yaitu library Klien Penagihan) dan mengonversi data StateFlow yang ditampilkan di BillingClientWrapper menjadi Alur.
  • ButtonModel adalah class data yang digunakan untuk membuat tombol di UI.
  • Composable mengekstrak semua metode composable UI ke dalam satu class.
  • MainState adalah class data untuk pengelolaan status.
  • MainViewModel digunakan untuk menyimpan status dan data terkait penagihan yang digunakan di UI. Ini menggabungkan semua alur di SubscriptionDataRepository menjadi satu objek status.
  • MainViewModelFactory adalah implementasi antarmuka ViewModelProviders.Factory yang digunakan untuk membuat instance MainViewModel.
  • Konstanta adalah objek yang memiliki konstanta yang digunakan oleh beberapa class.
  • MainActivity adalah class aktivitas utama yang memuat Composable untuk antarmuka pengguna.

Gradle

Anda perlu menambahkan dependensi Gradle untuk menambahkan Layanan Penagihan Google Play ke aplikasi Anda. Buka file build.gradle modul aplikasi, dan tambahkan yang berikut ini:

dependencies {
    val billing_version = "5.0.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Konsol

Untuk keperluan codelab ini, Anda perlu membuat dua penawaran produk langganan berikut di konsol Google Play:

  • 1 langganan dasar dengan ID produk up_basic_sub

Produk harus memiliki 3 paket dasar (2 perpanjangan otomatis dan 1 prabayar) dengan tag terkait : 1 langganan dasar bulanan dengan tag monthlybasic, 1 langganan dasar tahunan dengan tag yearlybasic, dan 1 langganan prabayar dengan tag prepaidbasic

Anda dapat menambahkan penawaran ke paket dasar. Penawaran akan mewarisi tag dari paket dasar yang terkait.

  • 1 langganan premium dengan ID produk up_premium_sub

Produk harus memiliki 3 paket dasar(2 perpanjangan otomatis dan 1 prabayar) dengan tag terkait: 1 langganan dasar bulanan dengan tag monthlypremium, 1 langganan dasar tahunan dengan tag yearlypremium, dan 1 langganan prabayar dengan tag prepaidpremium

Anda dapat menambahkan penawaran ke paket dasar. Penawaran akan mewarisi tag dari paket dasar yang terkait.

Untuk informasi yang lebih mendetail tentang cara membuat produk langganan, paket dasar, penawaran, dan tag, lihat Pusat Bantuan Google Play di sini.

4. Penyiapan Klien Penagihan

Untuk bagian ini, Anda akan mengerjakan class BillingClientWrapper.

Pada akhirnya, Anda akan memiliki semua yang diperlukan untuk membuat instance Klien Penagihan, dan semua metode terkait.

  1. Menginisialisasi BillingClient

Setelah menambahkan dependensi pada Library Layanan Penagihan Google Play, kita perlu menginisialisasi instance BillingClient.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Membuat koneksi ke Google Play

Setelah Anda membuat BillingClient, kami perlu menghubungkan Anda ke Google Play.

Agar terhubung ke Google Play, kami memanggil startConnection(). Proses koneksi bersifat asinkron, dan kita perlu mengimplementasikan BillingClientStateListener untuk menerima callback setelah penyiapan klien selesai dan siap untuk membuat permintaan lebih lanjut.

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. Membuat kueri Layanan Penagihan Google Play untuk pembelian yang ada

Setelah terhubung ke Google Play, kami siap membuat kueri untuk pembelian yang sebelumnya dilakukan pengguna dengan memanggil 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. Menampilkan produk yang tersedia untuk dibeli

Sekarang kita dapat mengkueri produk yang tersedia dan menampilkannya kepada pengguna. Untuk mengkueri detail produk langganan di Google Play, kita akan memanggil queryProductDetailsAsync(). Kueri untuk detail Produk merupakan langkah penting sebelum menampilkan produk kepada pengguna, karena proses ini akan menampilkan informasi produk yang dilokalkan.

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. Menetapkan Pemroses untuk kueri ProductDetails

Catatan: Metode ini memunculkan hasil kueri dalam Peta ke _productWithProductDetails.

Perhatikan juga bahwa kueri ini diharapkan menampilkan ProductDetails. Jika hal ini tidak terjadi, kemungkinan besar masalahnya adalah produk yang disiapkan di konsol Play belum diaktifkan atau Anda belum memublikasikan build dengan dependensi klien penagihan di jalur rilis mana pun.

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. Meluncurkan alur pembelian

launchBillingFlow adalah metode yang dipanggil saat pengguna mengklik untuk membeli item. Hal ini meminta Google Play untuk memulai alur pembelian dengan ProductDetails produk.

BillingClientWrapper.kt

fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
   if (!billingClient.isReady) {
       Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
   }
   billingClient.launchBillingFlow(activity, params)

}
  1. Menetapkan pemroses untuk hasil operasi pembelian

Saat pengguna keluar dari layar pembelian Google Play (dengan mengetuk tombol 'Beli' untuk menyelesaikan pembelian, atau dengan mengetuk tombol Kembali untuk membatalkan pembelian), callback onPurchaseUpdated() akan mengirimkan hasil alur pembelian kembali ke aplikasi Anda. Berdasarkan BillingResult.responseCode, Anda kemudian dapat menentukan apakah pengguna berhasil membeli produk atau tidak. Jika responseCode == OK, artinya pembelian berhasil diselesaikan.

onPurchaseUpdated() memberikan daftar objek Purchase yang mencakup semua pembelian yang dilakukan pengguna melalui aplikasi. Di antara banyak kolom lainnya, setiap objek Pembelian berisi atribut id produk, purchaseToken, dan isAcknowledged. Dengan kolom ini, untuk setiap objek Purchase, Anda dapat menentukan apakah ini merupakan pembelian baru yang perlu diproses atau pembelian yang sudah ada yang tidak memerlukan pemrosesan lebih lanjut.

Untuk pembelian langganan, pemrosesannya sama dengan mengonfirmasi pembelian baru.

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. Memproses pembelian (Memverifikasi dan Mengonfirmasi pembelian)

Setelah pengguna menyelesaikan pembelian, aplikasi kemudian harus memproses pembelian tersebut dengan mengonfirmasinya.

Selain itu, nilai _isNewPurchaseAcknowledged ditetapkan ke benar saat konfirmasi berhasil diproses.

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. Menghentikan koneksi penagihan

Terakhir, saat aktivitas dihancurkan, Anda ingin menghentikan koneksi ke Google Play, sehingga endConnection() dipanggil untuk melakukannya.

BillingClientWrapper.kt

fun terminateBillingConnection() {
   Log.i(TAG, "Terminating connection")
   billingClient.endConnection()
}

5. SubscriptionDataRepository

Di BillingClientWrapper, respons dari QueryPurchasesAsync dan QueryProductDetails masing-masing diposting ke MutableStateFlow _purchases dan _productWithProductDetails yang ditampilkan di luar class dengan pembelian dan productWithProductDetails.

Pada bulan SubscriptionDataRepository, pembelian diproses menjadi tiga Alur berdasarkan produk pembelian yang dikembalikan: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium, dan hasPremiumPrepaid.

Selain itu, productWithProductDetails diproses menjadi Flow basicProductDetails, dan premiumProductDetails masing-masing.

6. MainViewModel

Bagian yang sulit sudah selesai. Sekarang Anda akan menentukan MainViewModel, yang merupakan antarmuka publik untuk klien Anda sehingga mereka tidak perlu mengetahui internal BillingClientWrapper dan SubscriptionDataRepository.

Pertama di MainViewModel, kita memulai koneksi penagihan saat viewModel diinisialisasi.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Kemudian, Alur dari repositori masing-masing digabungkan menjadi productsForSaleFlows (untuk produk yang tersedia) dan userCurrentSubscriptionFlow (untuk langganan saat ini dan aktif milik pengguna) seperti yang diproses di class repo.

Daftar pembelian saat ini juga tersedia untuk UI di 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 gabungan dikumpulkan dalam blok init dan nilai diposting ke objek MutableLiveData yang disebut _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 juga menambahkan beberapa metode yang sangat berguna:

  1. Pengambilan token Paket Dasar dan Penawaran

Dimulai dengan Library Layanan Penagihan Play versi 5.0.0, semua produk langganan dapat memiliki beberapa paket dasar dan penawaran kecuali untuk paket dasar prabayar yang tidak dapat memiliki penawaran.

Metode ini membantu mengambil semua penawaran dan paket dasar yang valid bagi pengguna dengan menggunakan konsep tag yang baru diperkenalkan yang digunakan untuk mengelompokkan penawaran terkait.

Misalnya, saat pengguna mencoba membeli langganan dasar bulanan, semua paket dasar dan penawaran yang terkait dengan produk langganan dasar bulanan diberi tag dengan string 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. Penghitungan penawaran dengan harga terendah

Jika pengguna memenuhi syarat untuk beberapa penawaran, metode leastPricedOfferToken() digunakan untuk menghitung penawaran terendah di antara penawaran yang ditampilkan oleh retrieveEligibleOffers().

Metode ini menampilkan token ID penawaran dari penawaran yang dipilih.

Penerapan ini hanya menampilkan penawaran dengan harga terendah dalam hal kumpulan pricingPhases dan tidak memperhitungkan rata-rata.

Penerapan lainnya dapat berupa penawaran dengan harga rata-rata terendah.

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. Builder BillingFlowParams

Untuk meluncurkan alur pembelian produk tertentu, ProductDetails produk dan token penawaran yang dipilih perlu ditetapkan dan digunakan untuk membuat BilingFlowParams.

Ada dua metode untuk membantu hal ini:

upDowngradeBillingFlowParamsBuilder() membuat parameter untuk upgrade dan downgrade.

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() membuat parameter untuk pembelian normal.

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. Metode pembelian

Metode pembelian menggunakan launchBillingFlow() BillingClientWrapper dan BillingFlowParams untuk meluncurkan pembelian.

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. Menghentikan koneksi penagihan

Terakhir, metode terminateBillingConnection BillingClientWrapper dipanggil pada onCleared() ViewModel.

Hal ini untuk menghentikan koneksi penagihan saat ini ketika aktivitas terkait diakhiri.

7. UI

Sekarang, saatnya menggunakan semua yang telah Anda buat di UI. Untuk membantu hal tersebut, Anda akan menggunakan class Composable dan MainActivity.

Composable.kt

Class Composable disediakan sepenuhnya dan menentukan semua fungsi Compose yang digunakan untuk merender UI dan mekanisme navigasi di antara keduanya.

Fungsi Subscriptions menampilkan dua tombol: Basic Subscription dan Premium Subscription.

Basic Subscription dan Premium Subscription masing-masing memuat metode Compose baru yang menampilkan tiga paket dasar masing-masing: bulanan, tahunan, dan prabayar.

Kemudian, ada tiga kemungkinan fungsi tulis profil yang dapat digunakan oleh setiap pelanggan untuk langganan tertentu yang mungkin dimiliki pengguna: profil Dasar yang dapat diperpanjang, Premium yang dapat diperpanjang, dan profil Dasar Prabayar atau Premium Prabayar.

  • Jika pengguna memiliki langganan Dasar, profil dasar memungkinkan mereka mengupgrade ke langganan premium bulanan, tahunan, atau prabayar.
  • Sebaliknya, jika pengguna memiliki langganan premium, mereka dapat mendowngrade langganan bulanan, tahunan, atau prabayar.
  • Jika pengguna memiliki langganan prabayar, mereka dapat meningkatkan langganan mereka dengan tombol tambah saldo atau mengonversi langganan prabayar mereka menjadi paket dasar dengan perpanjangan otomatis.

Terakhir, ada fungsi Pemuatan layar yang digunakan saat koneksi dilakukan ke Google Play dan saat profil pengguna dirender.

MainActivity.kt

  1. Saat MainActivity dibuat, viewModel dibuat instance-nya dan fungsi compose yang disebut MainNavHost dimuat.
  2. MainNavHost dimulai dengan variabel isBillingConnected yang dibuat dari Livedata billingConnectionSate viewModel dan diamati perubahannya karena saat instance vieModel dibuat instance-nya, variabel tersebut meneruskan billingConnectionSate ke metode startBillingConnection BillingClientWrapper.

isBillingConnected disetel ke benar (true) saat koneksi dibuat dan salah (false) jika tidak.

Jika salah, fungsi LoadingScreen() compose akan dimuat, dan jika benar, fungsi Subscription atau profil akan dimuat.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Saat koneksi penagihan dibuat:

Instance navController sudah dibuat

val navController = rememberNavController()

Kemudian, flow di MainViewModel dikumpulkan.

val productsForSale by viewModel.productsForSaleFlows.collectAsState(
   initial = MainState()
)

val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
   initial = listOf()
)

Terakhir, variabel LiveData destinationScreen viewModel diamati.

Berdasarkan status langganan pengguna saat ini, fungsi tulis yang sesuai akan dirender.

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 Kode Solusi

Kode solusi lengkap dapat ditemukan di modul solusi.

9. Selamat

Selamat, Anda telah berhasil mengintegrasikan produk langganan Library Layanan Penagihan Google Play 5.0.0 ke dalam aplikasi yang sangat sederhana!

Untuk versi terdokumentasi aplikasi yang lebih canggih dengan penyiapan server yang aman, lihat contoh resmi.

Bacaan lebih lanjut