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.
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 helfenMainViewModel
: 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:
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
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.
- 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()
- 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)
}
})
}
- 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)
}
}
}
- 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)
}
}
}
- 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")
}
}
}
- 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)
}
- 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.
}
}
- 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
}
}
}
}
}
- 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:
- 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
}
- 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
}
- 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()
)
)
}
- 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.")
}
}
- 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
- Beim Erstellen von
MainActivity
wird viewModel instanziiert und eine Zusammensetzungsfunktion namensMainNavHost
wird geladen. MainNavHost
beginnt mit einer VariablenisBillingConnected
, die aus den LivedatabillingConnectionSate
von viewModel erstellt und auf Änderungen beobachtet wird, da bei der Instanziierung von vieModelbillingConnectionSate
an die Methode „startBillingConnection“ vonBillingClientWrapper
ü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()
- 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.