1. Introduction
Le système de facturation de Google Play est un service qui vous permet de vendre des produits et des contenus numériques dans votre application Android. Il s'agit du moyen le plus direct de vendre des produits intégrés pour monétiser votre application. Cet atelier de programmation vous explique comment utiliser la bibliothèque Google Play Billing pour vendre des abonnements dans votre projet en encapsulant tous les détails lorsque vous intégrez les achats au reste de votre application.
Il présente également des concepts liés aux abonnements, comme les forfaits de base, les offres, les tags et les forfaits prépayés. Pour en savoir plus sur les abonnements Google Play Billing, consultez notre Centre d'aide.
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez ajouter la dernière bibliothèque de facturation (version 5.0.0) à une application de profil simple basée sur un abonnement. L'application a déjà été conçue pour vous, vous n'allez donc ajouter que la partie facturation. Comme le montre la figure 1, dans cette application, l'utilisateur souscrit à l'un des forfaits de base et/ou aux offres proposés via deux abonnements renouvelables (de base et premium) ou à un forfait prépayé non renouvelable. C'est tout. Les forfaits de base correspondent respectivement à des abonnements mensuels et annuels. L'utilisateur peut passer à un forfait supérieur ou inférieur, ou passer d'un abonnement prépayé à un abonnement renouvelable.
Pour intégrer la bibliothèque Google Play Billing à votre application, vous devez créer les éléments suivants:
BillingClientWrapper
: wrapper pour la bibliothèque BillingClient Il vise à encapsuler les interactions avec le BillingClient de la bibliothèque Play Billing, mais il n'est pas requis dans votre propre intégration.SubscriptionDataRepository
: un référentiel de facturation pour votre application qui contient une liste de l'inventaire des produits sur abonnement de l'application (c'est-à-dire ce qui est à vendre), ainsi qu'une liste de variables ShareFlow qui permet de recueillir l'état des achats et les informations détaillées sur les produitsMainViewModel
: ViewModel via lequel le reste de votre application communique avec le dépôt de facturation. Il est utile de lancer le flux de facturation dans l'interface utilisateur à l'aide de différentes méthodes d'achat.
Une fois l'opération terminée, l'architecture de votre application doit ressembler à la figure ci-dessous:
Points abordés
- Intégrer la bibliothèque Play Billing
- Créer des produits sur abonnement, des forfaits de base, des offres et des tags via la Play Console
- Récupérer les forfaits de base et les offres disponibles dans l'application
- Lancer le flux de facturation avec les paramètres appropriés
- Proposer des produits sur abonnement prépayé
Cet atelier de programmation est consacré à Google Play Billing. Les concepts et les blocs de codes non pertinents ne sont pas abordés, et vous sont fournis afin que vous puissiez simplement les copier et les coller.
Prérequis
- Une version récente d'Android Studio (>= Arctic Fox | 2020.3.1)
- Un appareil Android équipé d'Android 8.0 ou version ultérieure
- L'exemple de code fourni sur GitHub (instructions dans les sections suivantes)
- Connaissance moyenne du développement Android sur Android Studio
- Vous disposez des connaissances nécessaires pour publier une application sur le Google Play Store.
- Expérience moyenne en écriture de code Kotlin
- Bibliothèque Google Play Billing version 5.0.0
2. Configuration
Obtenir le code depuis GitHub
Pour ce projet, nous avons regroupé tout ce dont vous avez besoin dans un dépôt Git. Pour commencer, vous devez récupérer le code et l'ouvrir dans votre environnement de développement préféré. Pour cet atelier de programmation, nous vous recommandons d'utiliser Android Studio.
Le code pour commencer est stocké dans un dépôt GitHub. Vous pouvez cloner le dépôt à l'aide de la commande suivante:
git clone https://github.com/android/play-billing-samples.git cd PlayBillingCodelab
3. Les fondations
Quel est notre point de départ ?
Notre point de départ est une application de profil utilisateur de base conçue pour cet atelier de programmation. Le code a été simplifié afin de montrer les concepts que nous voulons illustrer, et il n'a pas été conçu pour une utilisation en production. Si vous choisissez de réutiliser une partie de ce code dans une application de production, veillez à suivre les bonnes pratiques et à tester entièrement l'intégralité de votre code.
Importer le projet dans Android Studio
PlayBillingCodelab est l'application de base qui ne contient pas d'implémentation de Google Play Billing. Démarrez Android Studio et importez Billing-Codelab en sélectionnant Open > billing/PlayBillingCodelab
.
Le projet comporte deux modules :
- start dispose de l'application squelette, mais ne dispose pas des dépendances requises ni de toutes les méthodes à implémenter.
- finished dispose du projet terminé et peut vous servir de guide lorsque vous êtes bloqué.
L'application se compose de huit fichiers de classe: BillingClientWrapper
, SubscriptionDataRepository
, Composables
, MainState
, MainViewModel
, MainViewModelFactory
et MainActivity
.
- BillingClientWrapper est un wrapper qui isole les méthodes [BillingClient] de Google Play Billing nécessaires à une implémentation simple et émet des réponses dans le dépôt de données à des fins de traitement.
- SubscriptionDataRepository permet d'extraire la source de données Google Play Billing (c'est-à-dire la bibliothèque du client de facturation) et de convertir les données StateFlow émises dans BillingClientWrapper en flux.
- ButtonModel est une classe de données utilisée pour créer des boutons dans l'interface utilisateur.
- Composables extrait toutes les méthodes composables de l'UI en une seule classe.
- MainState est une classe de données permettant de gérer les états.
- MainViewModel permet de conserver les états et les données liés à la facturation utilisés dans l'UI. Elle combine tous les flux d'SubscriptionDataRepository en un seul objet d'état.
- MainActivity est la classe d'activité principale qui charge les composables pour l'interface utilisateur.
- Constantes est l'objet qui contient les constantes utilisées par plusieurs classes.
Gradle
Vous devez ajouter une dépendance Gradle pour ajouter Google Play Billing à votre application. Ouvrez le fichier build.gradle du module d'application et ajoutez les éléments suivants:
dependencies { val billing_version = "5.0.0" implementation("com.android.billingclient:billing:$billing_version") }
Google Play Console
Pour les besoins de cet atelier de programmation, vous devez créer les deux offres de produits sur abonnement suivantes dans la section Abonnements de la Google Play Console:
- 1 abonnement de base avec l'ID produit
up_basic_sub
Le produit doit être associé à trois forfaits de base (deux à renouvellement automatique et un prépayé) avec les balises associées : un abonnement de base mensuel avec la balise monthlybasic
, un abonnement de base annuel avec la balise yearlybasic
et un abonnement prépayé avec la balise prepaidbasic
Vous pouvez ajouter des offres aux forfaits de base. Les offres hériteront des tags des forfaits de base associés.
- 1 abonnement Premium avec l'ID produit
up_premium_sub
Le produit doit être associé à trois forfaits de base(deux à renouvellement automatique et un prépayé) avec les balises associées: un abonnement de base mensuel avec la balise monthlypremium
, un abonnement de base annuel avec la balise yearlypremium
et un abonnement prépayé avec la balise prepaidpremium
Vous pouvez ajouter des offres aux forfaits de base. Les offres hériteront des tags des forfaits de base associés.
Pour en savoir plus sur la création de produits sur abonnement, de forfaits de base, d'offres et de tags, consultez le Centre d'aide Google Play.
4. Configuration du client de facturation
Pour cette section, vous travaillerez dans la classe BillingClientWrapper
.
À la fin, vous aurez tout ce dont vous avez besoin pour instancier le client de facturation, ainsi que toutes les méthodes associées.
- Initialiser un BillingClient
Une fois que nous avons ajouté une dépendance à la bibliothèque Google Play Billing, nous devons initialiser une instance BillingClient
.
BillingClientWrapper.kt
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
- Établir une connexion avec Google Play
Une fois que vous avez créé un BillingClient, nous devons établir une connexion avec Google Play.
Pour associer Google Play, nous appelons startConnection()
. Le processus de connexion est asynchrone. Nous devons implémenter un BillingClientStateListener
pour recevoir un rappel une fois que la configuration du client est terminée et qu'il est prêt à envoyer d'autres requêtes.
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)
}
})
}
- Interroger Google Play Billing pour les achats existants
Une fois la connexion à Google Play établie, nous pouvons interroger les achats que l'utilisateur a précédemment effectués en appelant 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)
}
}
}
- Afficher les produits disponibles à l'achat
Nous pouvons maintenant rechercher les produits disponibles et les présenter aux utilisateurs. Pour interroger Google Play afin d'obtenir des informations sur un produit sur abonnement, nous appellerons queryProductDetailsAsync()
. L'interrogation des informations détaillées sur les produits est une étape importante avant de présenter les produits aux utilisateurs, car elle renvoie des informations localisées sur les produits.
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)
}
}
}
- Définir l'écouteur pour la requête ProductDetails
Remarque: Cette méthode émet le résultat de la requête dans un élément Map vers _productWithProductDetails
.
Notez également que la requête doit renvoyer ProductDetails
. Si ce n'est pas le cas, le problème est probablement dû au fait que les produits configurés dans la Play Console n'ont probablement pas été activés ou que vous n'avez publié aucun build avec la dépendance du client de facturation dans les canaux de publication.
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")
}
}
}
- Lancer le parcours d'achat
launchBillingFlow
est la méthode appelée lorsque l'utilisateur clique pour acheter un article. Google Play lance le parcours d'achat avec l'ProductDetails
du produit.
BillingClientWrapper.kt
fun launchBillingFlow(activity: Activity, params: BillingFlowParams) {
if (!billingClient.isReady) {
Log.e(TAG, "launchBillingFlow: BillingClient is not ready")
}
billingClient.launchBillingFlow(activity, params)
}
- Définir l'écouteur pour le résultat de l'opération d'achat
Lorsque l'utilisateur quitte l'écran d'achat Google Play (en appuyant sur le bouton "Acheter" pour finaliser l'achat ou en appuyant sur le bouton "Retour" pour annuler l'achat), le rappel onPurchaseUpdated()
renvoie le résultat du parcours d'achat à votre application. En vous basant sur la BillingResult.responseCode
, vous pouvez déterminer si l'utilisateur a bien acheté le produit. Si la valeur est responseCode == OK
, cela signifie que l'achat a bien été effectué.
onPurchaseUpdated()
renvoie une liste d'objets Purchase
incluant tous les achats effectués par l'utilisateur via l'application. Chaque objet "Purchase" contient, entre autres, les attributs ID produit, purchaseToken et isAcknowledged
. À l'aide de ces champs, pour chaque objet Purchase
, vous pouvez déterminer s'il s'agit d'un nouvel achat à traiter ou d'un achat existant qui ne nécessite aucun traitement supplémentaire.
Pour les achats d'abonnements, le traitement revient à confirmer le nouvel achat.
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.
}
}
- Traitement des achats (valider et confirmer les achats)
Une fois qu'un utilisateur a effectué un achat, l'application doit le traiter en le confirmant.
De plus, la valeur de _isNewPurchaseAcknowledged
est définie sur "true" lorsque la confirmation a bien été traitée.
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
}
}
}
}
}
- Mettre fin à la connexion de facturation
Enfin, lorsqu'une activité est détruite, vous devez mettre fin à la connexion à Google Play. endConnection()
est donc appelé pour cela.
BillingClientWrapper.kt
fun terminateBillingConnection() {
Log.i(TAG, "Terminating connection")
billingClient.endConnection()
}
5. SubscriptionDataRepository
Dans BillingClientWrapper
, les réponses de QueryPurchasesAsync
et QueryProductDetails
sont publiées respectivement dans MutableStateFlow
_purchases
et _productWithProductDetails
, qui sont exposés en dehors de la classe avec des achats et productWithProductDetails
.
Dans SubscriptionDataRepository
, les achats sont traités en trois flux en fonction du produit de l'achat renvoyé: hasRenewableBasic
, hasPrepaidBasic
, hasRenewablePremium
et hasPremiumPrepaid
.
De plus, productWithProductDetails
est traité en flux basicProductDetails
et premiumProductDetails
respectifs.
6. MainViewModel
Le plus dur est fait. Vous allez maintenant définir MainViewModel
, qui n'est qu'une interface publique pour vos clients afin qu'ils n'aient pas à connaître les composants internes de BillingClientWrapper
et SubscriptionDataRepository
.
Tout d'abord, dans MainViewModel
, nous démarrons la connexion de facturation lorsque le viewModel est initialisé.
MainViewModel.kt
init {
billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}
Ensuite, les flux du dépôt sont combinés respectivement dans productsForSaleFlows
(pour les produits disponibles) et userCurrentSubscriptionFlow
(pour l'abonnement actuel et actif de l'utilisateur) tels qu'ils sont traités dans la classe du dépôt.
La liste des achats en cours est également mise à la disposition de l'interface utilisateur avec 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
Le userCurrentSubscriptionFlow
combiné est collecté dans un bloc d'initialisation, et la valeur est publiée dans un objet MutableLiveData appelé _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
ajoute également des méthodes très utiles:
- Récupération des forfaits de base et des jetons d'offre
À partir de la version 5.0.0 de la bibliothèque Play Billing, tous les produits sur abonnement peuvent proposer plusieurs offres et forfaits de base, à l'exception des forfaits de base prépayés, qui ne peuvent pas comporter d'offres.
Cette méthode permet de récupérer toutes les offres et tous les forfaits de base auxquels un utilisateur est éligible grâce au nouveau concept de tags permettant de regrouper les offres associées.
Par exemple, lorsqu'un utilisateur tente de souscrire un abonnement de base mensuel, tous les forfaits de base et toutes les offres associés au produit sur abonnement de base mensuel sont tagués avec la chaîne 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
}
- Calcul de l'offre la moins chère
Lorsqu'un utilisateur est éligible à plusieurs offres, la méthode leastPricedOfferToken()
permet de calculer l'offre la moins élevée parmi celles renvoyées par retrieveEligibleOffers()
.
La méthode renvoie le jeton d'identifiant de l'offre sélectionnée.
Cette implémentation renvoie simplement les offres les moins chères en fonction de l'ensemble pricingPhases
et ne tient pas compte des moyennes.
Une autre implémentation pourrait consister à examiner l'offre moyenne la plus basse.
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
}
- Compilateurs BillingFlowParams
Pour lancer le parcours d'achat d'un produit particulier, le produit L'objet ProductDetails
et le jeton de l'offre sélectionnée doivent être définis et utilisés pour créer un BilingFlowParams.
Pour cela, deux méthodes s'offrent à vous:
upDowngradeBillingFlowParamsBuilder()
crée les paramètres des mises à niveau et des rétrogradations.
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()
crée les paramètres pour les achats normaux.
MainViewModel.kt
private fun billingFlowParamsBuilder(
productDetails: ProductDetails,
offerToken: String
): BillingFlowParams.Builder {
return BillingFlowParams.newBuilder().setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
)
)
}
- Méthode d'achat
La méthode d'achat utilise le launchBillingFlow()
de BillingClientWrapper
et le BillingFlowParams
pour lancer les achats.
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.")
}
}
- Mettre fin à la connexion de facturation
Enfin, la méthode terminateBillingConnection
de BillingClientWrapper
est appelée sur le onCleared()
d'un ViewModel.
Cela permet de mettre fin à la connexion de facturation actuelle lorsque l'activité associée est détruite.
7. Interface utilisateur
Il est maintenant temps d'utiliser tout ce que vous avez créé dans l'UI. Pour ce faire, vous utiliserez les classes composables et MainActivity.
Composables.kt
La classe des composables est entièrement fournie. Elle définit toutes les fonctions Compose utilisées pour afficher l'UI et le mécanisme de navigation entre elles.
La fonction Subscriptions
affiche deux boutons: Basic Subscription
et Premium Subscription
.
Basic Subscription
et Premium Subscription
chargent chacune de nouvelles méthodes Compose qui présentent les trois forfaits de base respectifs: mensuel, annuel et prépayé.
Ensuite, il existe trois fonctions de composition de profil possibles pour un abonnement spécifique d'un utilisateur: un profil Basic et Premium renouvelable, et un profil prépayé de base ou Premium.
- Lorsqu'un utilisateur dispose d'un abonnement Basic, le profil de base lui permet de passer à un abonnement Premium mensuel, annuel ou prépayé.
- À l'inverse, les utilisateurs disposant d'un abonnement Premium peuvent passer à un abonnement mensuel, annuel ou de base.
- Lorsqu'un utilisateur dispose d'un abonnement prépayé, il peut le créditer à l'aide du bouton de recharge ou convertir son abonnement prépayé en un forfait de base à renouvellement automatique correspondant.
Enfin, il existe une fonction d'écran de chargement utilisée lorsqu'une connexion à Google Play est établie et qu'un profil utilisateur est affiché.
MainActivity.kt
- Lorsque
MainActivity
est créé, le viewModel est instancié et une fonction Compose appeléeMainNavHost
est chargée. MainNavHost
commence par une variableisBillingConnected
créée à partir des données en directbillingConnectionSate
du viewModel et observe les modifications, car lorsque le vieModel est instancié, il transmetbillingConnectionSate
à la méthode startBillingConnection deBillingClientWrapper
.
isBillingConnected
est défini sur "true" lorsque la connexion est établie et sur "false" dans le cas contraire.
Si la valeur est "false", la fonction Compose LoadingScreen()
est chargée. Lorsqu'elle est définie sur "true", les fonctions Subscription
ou profile sont chargées.
val isBillingConnected by viewModel.billingConnectionState.observeAsState()
- Lorsqu'une connexion de facturation est établie:
Le navController
Compose est instancié
val navController = rememberNavController()
Les flux dans MainViewModel
sont ensuite collectés.
val productsForSale by viewModel.productsForSaleFlows.collectAsState(
initial = MainState()
)
val currentPurchases by viewModel.currentPurchasesFlow.collectAsState(
initial = listOf()
)
Enfin, la variable LiveData destinationScreen
du viewModel est observée.
La fonction Compose correspondante s'affiche en fonction de l'état d'abonnement actuel de l'utilisateur.
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. Code de solution
Le code complet de la solution est disponible dans le module correspondant.
9. Félicitations
Félicitations ! Vous avez intégré les produits sur abonnement de la bibliothèque Google Play Billing 5.0.0 dans une application très simple.
Pour obtenir une version documentée d'une application plus sophistiquée avec une configuration de serveur sécurisée, consultez l'exemple officiel.