Abos in Apps verkaufen – mit der Play Billing Library 5

1. Einführung

Das Abrechnungssystem von Google Play ist ein Dienst, mit dem Sie digitale Produkte und Inhalte in Ihrer Android-App verkaufen können. Es ist die direkteste Möglichkeit für Sie, In-App-Produkte zur Monetarisierung Ihrer App zu verkaufen. In diesem Codelab erfährst du, wie du die Google Play Billing Library verwendest, um Abos in deinem Projekt so zu verkaufen, dass du die Details bei der Integration von Käufen in den Rest deiner App erhältst.

Außerdem werden abobezogene Konzepte wie Basis-Abos, Angebote, Tags und Prepaid-Tarife vorgestellt. Weitere Informationen zu Abos bei Google Play Billing finden Sie in unserer Hilfe.

Inhalt

In diesem Codelab fügst du einer einfachen abobasierten Profil-App die neueste Billing Library (Version 5.0.0) hinzu. Da die App bereits für Sie entwickelt wurde, fügen Sie einfach den Abrechnungsteil hinzu. Wie in Abbildung 1 gezeigt, registriert sich der Nutzer in dieser App für eines der Basis-Abos und/oder Angebote, die über zwei verlängerbare Aboprodukte (Basic und Premium) oder für ein nicht verlängerbares Prepaid-Guthaben angeboten werden. Das ist alles. Die Basis-Abos sind Monats- bzw. Jahresabos. Der Nutzer kann ein Prepaid-Abo upgraden, downgraden oder in ein verlängerbares Abo umwandeln.

d7dba51f800a6cc4.png 2220c15b849d2ead.png

Um die Google Play Billing Library in deine App einzubinden, musst du Folgendes erstellen:

  • BillingClientWrapper: ein Wrapper für die BillingClient-Bibliothek. Sie dient zur Kapselung der Interaktionen mit dem BillingClient der Play Billing Library, ist in Ihrer eigenen Integration jedoch nicht erforderlich.
  • SubscriptionDataRepository: ein Abrechnungs-Repository für Ihre App, das eine Liste des Aboproduktinventars der App (d.h., was zum Verkauf angeboten wird) und eine Liste von ShareFlow-Variablen enthält, die beim Erfassen des Status von Käufen und Produktdetails helfen
  • MainViewModel: ein ViewModel, über das die restliche App mit dem Abrechnungs-Repository kommuniziert. Es ist hilfreich, den Abrechnungsablauf in der Benutzeroberfläche mit verschiedenen Kaufmethoden zu starten.

Wenn Sie fertig sind, sollte die Architektur Ihrer App in etwa so aussehen:

c83bc759f32b0a63.png

Aufgaben in diesem Lab

  • Play Billing Library integrieren
  • Aboprodukte, Basis-Abos, Angebote und Tags über die Play Console erstellen
  • Verfügbare Basis-Abos und Angebote aus der App abrufen
  • So starten Sie den Abrechnungsablauf mit den entsprechenden Parametern
  • So bieten Sie Produkte mit Prepaid-Tarif an

In diesem Codelab geht es um Google Play Billing. Auf irrelevante Konzepte wird nicht genauer eingegangen und entsprechende Codeblöcke können Sie einfach kopieren und einfügen.

Voraussetzungen

  • Eine aktuelle Version von Android Studio (>= Arctic Fox | 2020.3.1)
  • Android-Gerät mit Android 8.0 oder höher
  • Den Beispielcode, der auf GitHub für Sie bereitgestellt wird (Anleitung in späteren Abschnitten)
  • Mittelmäßige Kenntnisse der Android-Entwicklung in Android Studio
  • Kenntnisse zur Veröffentlichung von Apps im Google Play Store
  • Mittlere Erfahrung beim Schreiben von Kotlin-Code
  • Google Play Billing Library 5.0.0

2. Einrichtung

Code von GitHub abrufen

Wir haben alles, was Sie für dieses Projekt benötigen, in einem Git-Repository abgelegt. Zuerst müssen Sie den Code abrufen und in Ihrer bevorzugten Entwicklungsumgebung öffnen. Für dieses Codelab empfehlen wir die Verwendung von Android Studio.

Der Code für die ersten Schritte ist in einem GitHub-Repository gespeichert. Sie können das Repository mit dem folgenden Befehl klonen:

git clone https://github.com/android/play-billing-samples.git
cd PlayBillingCodelab

3. Die erste Phase

Was ist unser Ausgangspunkt?

Als Ausgangspunkt dient eine einfache App mit Nutzerprofilen, die für dieses Codelab entwickelt wurde. Der Code wurde vereinfacht, um die Konzepte zu zeigen, die wir veranschaulichen möchten. Er wurde nicht für die Produktion entwickelt. Wenn Sie einen Teil dieses Codes in einer Produktions-App wiederverwenden, sollten Sie sich an die Best Practices halten und Ihren gesamten Code vollständig testen.

Projekt in Android Studio importieren

PlayBillingCodelab ist die Basis-App, die keine Google Play Billing-Implementierung enthält. Starte Android Studio und importiere das Abrechnungs-Codelab, indem du Open > billing/PlayBillingCodelab auswählst

Das Projekt besteht aus zwei Modulen:

  • start enthält die grundlegende App, es fehlen jedoch die erforderlichen Abhängigkeiten und alle Methoden, die Sie implementieren müssen.
  • Fertig ist das abgeschlossene Projekt und kann als Anhaltspunkt dienen, wenn Sie nicht weiterkommen.

Die App besteht aus acht Klassendateien: BillingClientWrapper, SubscriptionDataRepository, Composables, MainState, MainViewModel, MainViewModelFactory und MainActivity.

  • BillingClientWrapper ist ein Wrapper, der die [BillingClient]-Methoden von Google Play Billing isoliert, die für eine einfache Implementierung erforderlich sind, und Antworten an das Daten-Repository zur Verarbeitung ausgibt.
  • SubscriptionDataRepository wird verwendet, um die Google Play Billing-Datenquelle (also die Billing Client-Bibliothek) zu abstrahieren und die in BillingClientWrapper ausgegebenen StateFlow-Daten in Flows zu konvertieren.
  • ButtonModel ist eine Datenklasse zum Erstellen von Schaltflächen in der Benutzeroberfläche.
  • Composables extrahiert alle zusammensetzbaren Methoden der UI in eine Klasse.
  • MainState ist eine Datenklasse für die Zustandsverwaltung.
  • MainViewModel wird verwendet, um abrechnungsbezogene Daten und Status zu speichern, die in der Benutzeroberfläche verwendet werden. Es kombiniert alle Abläufe im SubscriptionDataRepository in einem Statusobjekt.
  • MainActivity ist die Hauptaktivitätsklasse, die die Composables für die Benutzeroberfläche lädt.
  • Konstanten ist das Objekt mit den Konstanten, die von mehreren Klassen verwendet werden.

Gradle

Sie müssen eine Gradle-Abhängigkeit hinzufügen, um Ihrer App Google Play Billing hinzuzufügen. Öffnen Sie die build.gradle-Datei des App-Moduls und fügen Sie Folgendes hinzu:

dependencies {
    val billing_version = "5.0.0"

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

Google Play Console

Für dieses Codelab müssen Sie die folgenden beiden Aboprodukte im Bereich „Abos“ der Google Play Console erstellen:

  • 1 Basisabo mit der Produkt-ID up_basic_sub

Das Produkt sollte 3 Basis-Abos (2 mit automatischer Verlängerung und 1 Vorauszahlung) mit zugehörigen Tags haben : 1 monatliches Basisabo mit dem Tag monthlybasic, 1 Basis-Jahresabo mit dem Tag yearlybasic und 1 Vorauszahlungsabo mit dem Tag prepaidbasic

Du kannst den Basis-Abos Angebote hinzufügen. Angebote übernehmen die Tags von den zugehörigen Basis-Abos.

  • 1 Premium-Abo mit der Produkt-ID up_premium_sub

Das Produkt sollte 3 Basis-Abos(2 mit automatischer Verlängerung und 1 Vorauszahlung) mit zugehörigen Tags haben: 1 monatliches Basisabo mit dem Tag monthlypremium, 1 Basis-Jahresabo mit dem Tag yearlypremium und 1 Vorauszahlungsabo mit dem Tag prepaidpremium

a9f6fd6e70e69fed.png

Du kannst den Basis-Abos Angebote hinzufügen. Angebote übernehmen die Tags von den zugehörigen Basis-Abos.

Weitere Informationen zum Erstellen von Aboprodukten, Basis-Abos, Angeboten und Tags finden Sie in der Google Play-Hilfe.

4. Der für den Abrechnungsclient eingerichtete

In diesem Abschnitt arbeiten Sie in der Klasse BillingClientWrapper.

Am Ende verfügen Sie über alles, was für die Instanziierung des Abrechnungsclients erforderlich ist, sowie über alle zugehörigen Methoden.

  1. BillingClient initialisieren

Sobald wir eine Abhängigkeit von der Google Play Billing Library hinzugefügt haben, müssen wir eine BillingClient-Instanz initialisieren.

BillingClientWrapper.kt

private val billingClient = BillingClient.newBuilder(context)
   .setListener(this)
   .enablePendingPurchases()
   .build()
  1. Verbindung mit Google Play herstellen

Nachdem Sie einen BillingClient erstellt haben, müssen wir eine Verbindung zu Google Play herstellen.

Zur Verbindung mit Google Play rufen wir startConnection() an. Der Verbindungsvorgang ist asynchron und wir müssen eine BillingClientStateListener implementieren, um einen Callback zu erhalten, sobald die Einrichtung des Clients abgeschlossen ist und er für weitere Anfragen bereit ist.

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 für bestehende Käufe abfragen

Nachdem eine Verbindung zu Google Play hergestellt wurde, können wir Käufe abfragen, die der Nutzer zuvor durch Aufrufen von queryPurchasesAsync() getätigt hat.

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. Zum Kauf verfügbare Produkte anzeigen

Wir können nun verfügbare Produkte abfragen und den Nutzenden anzeigen. Um Aboproduktdetails bei Google Play abzufragen, rufen wir queryProductDetailsAsync() auf. Das Abfragen von Produktdetails ist ein wichtiger Schritt, bevor die Produkte den Nutzern angezeigt werden, da dadurch lokalisierte Produktinformationen zurückgegeben werden.

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 für die Abfrage „ProductDetails“ festlegen

Hinweis: Diese Methode gibt das Ergebnis der Abfrage in einer Map an _productWithProductDetails aus.

Außerdem wird erwartet, dass die Abfrage ProductDetails zurückgibt. Wenn dies nicht der Fall ist, liegt das wahrscheinlich daran, dass in der Play Console eingerichtete Produkte nicht aktiviert wurden oder Sie keinen Build mit der Abhängigkeit des Abrechnungsclients in einem der Release-Tracks veröffentlicht haben.

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. Kaufvorgang starten

launchBillingFlow ist die Methode, die aufgerufen wird, wenn der Nutzer klickt, um einen Artikel zu kaufen. Google Play wird aufgefordert, den Kaufvorgang mit der ProductDetails des Produkts zu starten.

BillingClientWrapper.kt

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

}
  1. Legen Sie den Listener für das Ergebnis des Kaufvorgangs fest.

Wenn der Nutzer den Google Play-Kaufbildschirm verlässt (entweder durch Tippen auf die Schaltfläche „Kaufen“, um den Kauf abzuschließen, oder durch Tippen auf die Schaltfläche „Zurück“, um den Kauf zu stornieren), sendet der onPurchaseUpdated()-Callback das Ergebnis des Kaufvorgangs an deine App zurück. Anhand der BillingResult.responseCode können Sie dann feststellen, ob der Nutzer das Produkt erfolgreich gekauft hat. Wenn responseCode == OK, wurde der Kauf erfolgreich abgeschlossen.

onPurchaseUpdated() gibt eine Liste von Purchase-Objekten zurück, die alle Käufe des Nutzers über die App enthält. Neben vielen anderen Feldern enthält jedes Kaufobjekt die Attribute „product id“, „purchaseToken“ und „isAcknowledged“. Mithilfe dieser Felder können Sie dann für jedes Purchase-Objekt bestimmen, ob es sich um einen neuen Kauf handelt, der verarbeitet werden muss, oder um einen vorhandenen Kauf, der nicht weiter verarbeitet werden muss.

Bei Abokäufen entspricht die Verarbeitung der Bestätigung des neuen Kaufs.

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. Käufe verarbeiten (Käufe bestätigen und bestätigen)

Sobald ein Nutzer einen Kauf abgeschlossen hat, muss die App diesen Kauf durch Bestätigung verarbeiten.

Außerdem wird der Wert von _isNewPurchaseAcknowledged auf „true“ gesetzt, wenn die Bestätigung erfolgreich verarbeitet wurde.

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. Abrechnungsverbindung beenden

Wenn schließlich eine Aktivität gelöscht wird, möchten Sie die Verbindung zu Google Play beenden. Daher wird endConnection() aufgerufen, um dies zu tun.

BillingClientWrapper.kt

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

5. SubscriptionDataRepository

Im BillingClientWrapper werden Antworten von QueryPurchasesAsync und QueryProductDetails an MutableStateFlow _purchases und _productWithProductDetails gepostet, die außerhalb des Kurses mit Käufen und productWithProductDetails angezeigt werden.

In SubscriptionDataRepository werden Käufe basierend auf dem zurückgegebenen Produkt in drei Abläufe verarbeitet: hasRenewableBasic, hasPrepaidBasic, hasRenewablePremium und hasPremiumPrepaid.

Außerdem wird productWithProductDetails in die entsprechenden basicProductDetails- und premiumProductDetails-Abläufe verarbeitet.

6. MainViewModel

Der schwierige Teil ist geschafft. Als Nächstes definieren Sie die MainViewModel. Dies ist nur eine öffentliche Schnittstelle für Ihre Kunden, damit sie nicht mit den internen Strukturen von BillingClientWrapper und SubscriptionDataRepository vertraut sind.

Zuerst wird in der MainViewModel die Abrechnungsverbindung gestartet, wenn „viewModel“ initialisiert wird.

MainViewModel.kt

init {
   billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}

Dann werden die Abläufe aus dem Repository entsprechend der Verarbeitung in der Repository-Klasse in productsForSaleFlows (für verfügbare Produkte) und userCurrentSubscriptionFlow (für das aktuelle und aktive Abo des Nutzers) zusammengefasst.

Die Liste der aktuellen Käufe wird auch über die Benutzeroberfläche mit currentPurchasesFlow verfügbar gemacht.

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

Die kombinierte userCurrentSubscriptionFlow wird in einem Init-Block erfasst und der Wert an ein MutableLiveData-Objekt namens _destinationScreen gesendet.

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 bietet außerdem einige sehr hilfreiche Methoden:

  1. Abruf von Basis-Abos und Angebotstokens

Ab Version 5.0.0 der Play Billing Library können alle Aboprodukte mehrere Basis-Abos und Angebote haben, mit Ausnahme von Basis-Abos mit Vorauszahlung, die keine Angebote enthalten dürfen.

Mit dieser Methode lassen sich alle Angebote und Basis-Abos abrufen, für die ein Nutzer infrage kommt. Dazu wird das neu eingeführte Konzept von Tags zum Gruppieren ähnlicher Angebote verwendet.

Wenn ein Nutzer beispielsweise versucht, ein monatliches Basis-Abo zu kaufen, werden alle Basis-Abos und Angebote, die mit dem Basis-Monatsabo verknüpft sind, mit dem String monthlyBasic getaggt.

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. Berechnung des günstigsten Angebots

Wenn ein Nutzer für mehrere Angebote infrage kommt, wird die Methode leastPricedOfferToken() verwendet, um unter den von retrieveEligibleOffers() zurückgegebenen Angeboten das niedrigste Angebot zu berechnen.

Die Methode gibt das Angebots-ID-Token des ausgewählten Angebots zurück.

Bei dieser Implementierung werden einfach die günstigsten Angebote in Bezug auf den pricingPhases-Satz zurückgegeben und Durchschnittswerte werden nicht berücksichtigt.

Eine andere Implementierung könnte das günstigste Angebot zum durchschnittlichen Preis sein.

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

Um den Kaufvorgang für ein bestimmtes Produkt zu starten, ProductDetails und das Token des ausgewählten Angebots müssen festgelegt und verwendet werden, um BilingFlowParams zu erstellen.

Dafür gibt es zwei Methoden:

upDowngradeBillingFlowParamsBuilder() erstellt die Parameter für Upgrades und Downgrades.

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() erstellt die Parameter für normale Käufe.

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. Kaufmethode

Die Kaufmethode verwendet die launchBillingFlow() von BillingClientWrapper und die BillingFlowParams, um Käufe zu starten.

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. Abrechnungsverbindung beenden

Schließlich wird die terminateBillingConnection-Methode der BillingClientWrapper auf der onCleared() eines ViewModel-Elements aufgerufen.

Hiermit wird die aktuelle Abrechnungsverbindung beendet, wenn die zugehörige Aktivität gelöscht wird.

7. Die Benutzeroberfläche

Jetzt ist es an der Zeit, alle in der Benutzeroberfläche erstellten UI-Elemente zu verwenden. Dafür arbeiten Sie mit den Klassen Composables und MainActivity.

Composables.kt

Die Composables-Klasse wird vollständig bereitgestellt und definiert alle Compose-Funktionen, die zum Rendern der Benutzeroberfläche und des dazwischen liegenden Navigationsmechanismus verwendet werden.

Die Funktion Subscriptions enthält zwei Schaltflächen: Basic Subscription und Premium Subscription.

Für Basic Subscription und Premium Subscription werden jeweils neue Erstellungsmethoden geladen, die jeweils die drei Basis-Abos enthalten: monatlich, jährlich und im Voraus.

Dann gibt es drei mögliche Profilerstellungsfunktionen, jeweils für ein bestimmtes Abo, das ein Nutzer haben kann: ein verlängerbares Basic, ein erneuerbares Premium-Profil und entweder ein Prepaid Basic- oder Prepaid Premium-Profil.

  • Nutzer mit Basis-Abo können mit dem Basisprofil ein Upgrade auf ein Monats-, Jahres- oder Prepaid-Premium-Abo ausführen.
  • Umgekehrt kann ein Nutzer mit einem Premium-Abonnement ein Downgrade auf ein Monats-, Jahresabo oder ein Prepaid-Basisabo ausführen.
  • Wenn ein Nutzer ein Prepaid-Abo hat, kann er es mit der Aufladeschaltfläche aufladen oder sein Prepaid-Abo in ein entsprechendes Basis-Abo mit automatischer Verlängerung umwandeln.

Außerdem gibt es eine Ladebildschirmfunktion, die verwendet wird, wenn eine Verbindung zu Google Play hergestellt wird und ein Nutzerprofil gerendert wird.

Hauptaktivität.kt

  1. Beim Erstellen von MainActivity wird viewModel instanziiert und eine Zusammensetzungsfunktion namens MainNavHost wird geladen.
  2. MainNavHost beginnt mit einer Variablen isBillingConnected, die aus den Livedata billingConnectionSate von viewModel erstellt und auf Änderungen beobachtet wird, da bei der Instanziierung von vieModel billingConnectionSate an die Methode „startBillingConnection“ von BillingClientWrapper übergeben wird.

isBillingConnected wird auf „true“ gesetzt, wenn die Verbindung hergestellt wurde, und auf „false“, wenn dies nicht der Fall ist.

Bei „false“ wird die Erstellungsfunktion LoadingScreen() geladen, bei „true“ werden die Subscription- oder Profilfunktionen geladen.

val isBillingConnected by viewModel.billingConnectionState.observeAsState()
  1. Wenn eine Abrechnungsverbindung hergestellt wird:

navController für „Compose“ wurde instanziiert

val navController = rememberNavController()

Anschließend werden die Abläufe in MainViewModel erfasst.

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

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

Schließlich wird die LiveData-Variable destinationScreen von viewModel beobachtet.

Basierend auf dem aktuellen Abostatus des Nutzers wird die entsprechende Erstellungsfunktion gerendert.

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. Lösungscode

Den vollständigen Lösungscode finden Sie im Lösungsmodul.

9. Glückwunsch

Herzlichen Glückwunsch! Sie haben die Abonnementprodukte von Google Play Billing Library 5.0.0 erfolgreich in eine sehr einfache App integriert.

Eine dokumentierte Version einer komplexeren App mit einer sicheren Servereinrichtung finden Sie im offiziellen Beispiel.

Weitere Informationen