Table of contents
My app on the Google Playstore
GitHub code
Resources I used
Before we start
- Before you can read any further, you must have done the following 2 things
1) Properly setup your google developer accound : The instructions can be found HERE
2) Add the Google billing library : The library can be found HERE. You need to add this library to your gradle.build file, then publish your app to either production, internal testing or closed testing.
Determine if the user is subscribed or not
So assuming you have done all the previously mentioned things, we can now worry about how to determine if the user is subscribed or not.
The first thing we are going to do is to create a class called BillingClientWrapper and this class is where we will have all the code the interacts directly with the Google Play Billing Library
Creating state inside the BillingClientWrapper
- Now we need to create a MutableState object that will hold our subscribed information. We then need to map it to a State object and expose it to the rest of our code:
class BillingClientWrapper(
context: Context
): PurchasesUpdatedListener{
// Current Purchases
private val _purchases =
MutableStateFlow<List<Purchase>>(listOf())
val purchases = _purchases.asStateFlow()
}
- This mapping from MutableState to State and then exposing as a public variable called
purchases
is a good coding practice. It allows us to define clear boundaries between our code.
for the moment we can ignore the PurchasesUpdatedListener
interface. But don't worry we will talk about it shortly
Initialize a BillingClient
- Everything starts with the BillingClient. We initialize the BillingClient like so:
// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
- BillingClient is the main interface for communication between the Google Play Billing Library and the rest of your app. BillingClient provides convenience methods, both synchronous and asynchronous, for many common billing operations. It's strongly recommended that we have one active BillingClient connection open at one time to avoid multiple PurchasesUpdatedListener callbacks for a single event
In .setListener(this)
the this
is referring to the PurchasesUpdatedListener
interface. Since it is an interface, we have to implement its method, which is the onPurchasesUpdated method:
override fun onPurchasesUpdated(
billingResult: BillingResult, //contains the response code from the In-App billing API
purchases: List<Purchase>? // a list of objects representing in-app purchases
) {
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)// dont need to worry about
}
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
Timber.tag("BILLINGR").e("User has cancelled")
} else {
// Handle any other error codes.
}
}
- The code above is responsible for updating our
_purchases
variable from earlier and will run each time our user makes a purchase. I also want to point out the the section of the code I markedacknowledgePurchases(purchase)// dont need to worry about
. At this moment we don't need to worry about this code. Because it is used for detecting the different states of the Purchase objects(I will talk about in a future tutorial). But if you still want to checkout the code yourself, HERE it is.
Connect to Google play
- After we have created a BillingClient, you need to establish a connection to Google Play. To connect to Google Play, call startConnection(). The connection process is asynchronous, and we must implement a BillingClientStateListener to receive a callback once the setup of the client is complete and itβs ready to make further requests:
/*******CALLED TO INITIALIZE EVERYTHING******/
// Establish a connection to Google Play.
fun startBillingConnection(billingConnectionState: MutableLiveData<Boolean>) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Timber.tag("BILLINGR").d("Billing response OK")
// The BillingClient is ready. You can query purchases and product details here
queryPurchases()// we talk about this in the next paragraph
} else {
Timber.tag("BILLINGR").e(billingResult.debugMessage)
}
}
override fun onBillingServiceDisconnected() {
Timber.tag("BILLINGR").d("Billing connection disconnected")
startBillingConnection(billingConnectionState)
}
})
}
- Inside the code block above you can see we create a anonymous class with
billingClient.startConnection(object : BillingClientStateListener
. Which means we are creating a instance of the BillingClientStateListener interface. Meaning we have to override it's two methods.
1) onBillingSetupFinished() : called when the connection to Google play is done and we can query for the user's purchases.
2) onBillingServiceDisconnected() : called when the connection to Google play billing service is lost. when this happens we just try to reconnect with a recursive call to startBillingConnection(billingConnectionState)
The queryPurchases() method
- Now we can talk about
queryPurchases()
:
fun queryPurchases() {
if (!billingClient.isReady) {
Timber.tag("BILLINGR").e("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 {
Timber.tag("BILLINGR").e(billingResult.debugMessage)
}
}
}
If you think this code seems similar to the
BillingClientStateListener
you're right. But as the documentation states, there are cases(such as buying a subscription outside of your app) where your app will be made aware of purchases by calling BillingClient.queryPurchasesAsync().Next we are using
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
to query the Google library for any subscriptions that the user has made.
Repository Layer
- Now we can create a repository layer that will be used as a filter and only return the subscriptions needed by our app.
class SubscriptionDataRepository(billingClientWrapper: BillingClientWrapper) {
// Set to true when a returned purchase is an auto-renewing basic subscription.
//hasRenewablePremium IS HOW WE WILL DETERMINE IF THERE IS A SUBSCRIPTION OR NOT
val hasRenewablePremium: Flow<Boolean> = billingClientWrapper.purchases.map { value: List<Purchase> ->
value.any { purchase: Purchase -> purchase.products.contains(PREMIUM_SUB) && purchase.isAutoRenewing}
}
companion object {
// List of subscription product offerings
private const val PREMIUM_SUB = "your_subscription_id"
}
}
- The important things from the code above are the
isAutoRenewing
andcontains(PREMIUM_SUB)
.isAutoRenewing
is a flag stored on a Purchase object to determine if the subscription is active or not.contains(PREMIUM_SUB)
is used as a filter and you need to put your ownsubscription_id
for you subscription. A subscription id can be found inside of your google play console underMonetization -> Subscriptions
.
ViewModel layer
- Now we are in the ViewModel there are 6 things that we need to do:
1) Initialize the BillingClientWrapper
2) Initialize the Repository
3) Create state to hold the repository data
4) Create a method to query the repository
5) Start the connection
6) Using DisposableEffect to call queryPurchasesAsync()
1) Initialize the BillingClientWrapper
- We know all of the code to talk to the Google play Billing library lives inside of the
BillingClientWrapper
. So the first thing inside of our ViewModel is to initialize it:
class BillingViewModel(application: Application): AndroidViewModel(application){
var billingClient: BillingClientWrapper = BillingClientWrapper(application)
}
- Notice how we use the
AndroidViewModel(application)
instead of the traditional ViewModel(). We do this to allow ourBillingViewModel
access to the application context, which we use to initialize theBillingClientWrapper
.
2) Initialize the Repository
- The next step is to use the initialized billingClient and pass it to the repository:
private var repo: SubscriptionDataRepository =
SubscriptionDataRepository(billingClientWrapper = billingClient)
3) Create state to hold the repository data
- We now need a object to hold the state we get from our repository and expose it to our view:
data class BillingUiState(
val subscribed:Boolean = false
)
class BillingViewModel(application: Application): AndroidViewModel(application){
private val _uiState = mutableStateOf(BillingUiState())
val state:State<BillingUiState> = _uiState
private var billingClient: BillingClientWrapper = BillingClientWrapper(application)
private var repo: SubscriptionDataRepository =
SubscriptionDataRepository(billingClientWrapper = billingClient)
}
4) Create a method to query our Repository
fun refreshPurchases(){
viewModelScope.launch {
repo.hasRenewablePremium.collect { collectedSubscriptions ->
_uiState.value = _uiState.value.copy(
subscribed = collectedSubscriptions
)
}
}
}
- As you can see from the code above, we are using the viewModelScope to
collect{}
from the Flow stored inside of our repository layerrepo.hasRenewablePremium
.
5) Start the connection
- Now that we have all of our methods set up we need to initialize everything inside of the BillingViewModel's init{} blocks:
init {
billingClient.startBillingConnection(billingConnectionState = _billingConnectionState)
}
init{
refreshPurchases()
}
- Technically when the our BillingViewModel is initialized, our code will be able to recognize if the user has bought any subscriptions or not. But there are many more situations we are not accounting for, such as the ones described in the documentation, HERE. As the documentation states, to be able to handle those situations we need to call the BillingClient.queryPurchasesAsync() in our onResume() method.
6) Using DisposableEffect to call queryPurchasesAsync()
- We can accomplish calling BillingClient.queryPurchasesAsync() in our onResume() by using Side-effects. More specifically we are going to use DisposableEffect which allows us to create, add and remove a lifecycle observe to the
onResume()
method. We can actually do so inside of a Compose fuctions:
@Composable
fun AddObserver(viewModel:BillingViewModel){
val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
Timber.tag("LIFEEVENTCYCLE").d("WE ARE RESUMING")
viewModel.refreshPurchases()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
- In order to make this code work we first need to update the
refreshPurchases()
method inside of the BillingViewModel:
fun refreshPurchases(){
viewModelScope.launch {
billingClient.queryPurchases()
repo.hasRenewablePremium.collect { collectedSubscriptions ->
//val value = collectedSubscriptions.hasRenewablePremium
_uiState.value = _uiState.value.copy(
subscribed = collectedSubscriptions
)
}
}
}
- Notice that the code above now calls the
billingClient.queryPurchases()
, which will update the_purchases
variable inside of the BillingClientWrapper, which will update thehasRenewablePremium
variable inside of the repository, which will then be collected in the BilingViewModel, which can then be shown to the use as the variablestate.subscribed
What is the next tutorial
- In the next tutorial we will implement the code to allow the user to buy our subscriptions
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)