A Kotlin Android In-App Purchasing Tutorial

In the previous chapter, we explored how to integrate in-app purchasing into an Android project and also looked at some code samples that can be used when working on your own projects. This chapter will put this theory into practice by creating an example project that demonstrates how to add a consumable in-app product to an Android app. The tutorial will also show how in-app products are added and managed within the Google Play Console and explain how to enable test payments so that purchases can be made during testing without having to spend real money.

About the In-App Purchasing Example Project

The simple concept behind this project is an app in which an in-app product must be purchased before a button can be clicked. This in-app product is consumed each time the button is clicked, requiring the user to re-purchase the product each time they want to be able to click the button. On initialization, the app will connect to the app store, obtain details of the product, and display the product name. Once the app has established that the product is available, a purchase button will be enabled which, when clicked, will step through the purchase process. On completion of the purchase, the main button will be enabled so that the user can click on it and consume the purchase.

Creating the InAppPurchase Project

The first step in this exercise is to create a new project. Begin by launching Android Studio and selecting the New Project option from the welcome screen. In the new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter InAppPurchase into the Name field and specify a package name that will uniquely identify your app within the Google Play ecosystem (for example com.<your company>.InAppPurchase). Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin. Once the project has been created, use the steps outlined in section 11.8 Migrating a Project to View Binding to convert the project to use view binding.

Adding Libraries to the Project

Before we start writing code, some libraries need to be added to the project build configuration, one of which is the standard Android billing client library. Later in the project, we will also need to use the ImmutableList class which is part of Google’s Guava Core Java libraries. Add these libraries now by modifying the Gradle Scripts -> build.gradle (Module: InAppPurchase.app) file with the following changes:

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

.
.
dependencies {
.
.
    implementation 'com.android.billingclient:billing:5.0.0'
    implementation 'com.android.billingclient:billing-ktx:5.0.0'
    implementation 'com.google.guava:guava:24.1-jre'
    implementation 'com.google.guava:guava:27.0.1-android'
.
.

Click on the Sync Now link at the top of the editor panel to commit these changes.

Designing the User Interface

The user interface will consist of the existing TextView and two Buttons. With the activity_main.xml file loaded into the editor, drag and drop two Button views onto the layout so that one is above and the other below the TextView. Select the TextView and change the id attribute to statusText.

Click on the Clear all Constraints button in the toolbar and shift-click to select all three views. Right-click on the top-most Button view and select the Center -> Horizontally in Parent menu option. Repeat this step once more, this time selecting Chains -> Create Vertical Chain. Change the text attribute of the top button so that it reads “Consume Purchase” and the id to consumeButton. Also, configure the onClick property to call a method named consumePurchase.

Select the bottom-most button and repeat the above steps, this time setting the text to “Buy Product”, the id to buyButton, and the onClick callback to makePurchase. Once completed, the layout should match that shown in Figure 84-1:

Figure 1-1

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Adding the App to the Google Play Store

Using the steps outlined in the chapter entitled “Creating, Testing and Uploading an Android App Bundle”, sign into the Play Console, create a new app, and set up a new internal testing track including the email addresses of designated testers. Return to Android Studio and generate a signed release app bundle for the project. Once the bundle file has been generated, upload it to the internal testing track and roll it out for testing.

Now that the app has a presence in the Google Play Store, we are ready to create an in-app product for the project.

Creating an In-App Product

With the app selected in the Play Console, scroll down the list of options in the left-hand panel until the Monetize section comes into view. Within this section, select the In-app products option listed under Products as shown in Figure 84-2:

Figure 1-2

On the In-app products page, click on the Create product button:

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 1-3

On the new product screen, enter the following information before saving the new product:

  • Product ID: one_button_click
  • Name: A Button Click
  • Description: This is a test in-app product that allows a button to be clicked once.
  • Default price: Set to the lowest possible price in your preferred currency.

Enabling License Testers

When testing in-app billing it is useful to be able to make test purchases without spending any money. This can be achieved by enabling license testing for the internal track testers. License testers can use a test payment card when making purchases so that they are not charged.

Within the Play Console, return to the main home screen and select the Setup -> License testing option:

Figure 1-4

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Within the license testing screen, add the testers that were added for the internal testing track, change the License response setting to RESPOND_NORMALLY, and save the changes:

Figure 1-5

Now that both the app and the in-app product have been set up in the Play Console, we can start adding code to the project.

Initializing the Billing Client

Edit the MainActivity.kt file and make the following changes to begin implementing the in-app purchase functionality:

.
.
import android.util.Log
import com.android.billingclient.api.*
.
.
class MainActivity : AppCompatActivity() {
 
    private lateinit var binding: ActivityMainBinding
    private lateinit var billingClient: BillingClient
    private lateinit var productDetails: ProductDetails
    private lateinit var purchase: Purchase
    private val demo_product = "one_button_click"
 
    val TAG = "InAppPurchaseTag"
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        billingSetup()
    }
 
    private fun billingSetup() {
        billingClient = BillingClient.newBuilder(this)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build()
            
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(
                billingResult: BillingResult
            ) {
                if (billingResult.responseCode ==
                    BillingClient.BillingResponseCode.OK
                ) {
                    Log.i(TAG, "OnBillingSetupFinish connected")
                    queryProduct(demo_product)
                } else {
                    Log.i(TAG, "OnBillingSetupFinish failed")
                }
            }
 
            override fun onBillingServiceDisconnected() {
                Log.i(TAG, "OnBillingSetupFinish connection lost")
            }
        })
    }
.
.

When the app starts, the onCreate() method will now call billingSetup() which will, in turn, create a new billing client instance and attempt to connect to the Google Play Billing Library. The onBillingSetupFinished() listener will be called when the connection attempt completes and output Logcat messages indicating the success or otherwise of the connection attempt. Finally, we have also implemented the onBillingServiceDisconnected() callback which will be called if the Google Play Billing Library connection is lost.

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

If the connection is successful a method named queryProduct() is called. Both this method and the purchasesUpdatedListener assigned to the billing client now need to be added.

Querying the Product

To make sure the product is available for purchase, we need to create a QueryProductDetailsParams instance configured with the product ID that was specified in the Play Console, and pass it to the queryProductDetailsAsync() method of the billing client. This will require that we also add the onProductDetailsResponse() callback method where we will check that the product exists, extract the product name, and display it on the status TextView. Now that we have obtained the product details, we can also safely enable the buy button. Within the MainActivity.kt file, add the queryProduct() method so that it reads as follows:

.
.
import com.google.common.collect.ImmutableList
.
.
private fun queryProduct(productId: String) {
    val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
        .setProductList(
            ImmutableList.of(
                Product.newBuilder()
                    .setProductId(productId)
                    .setProductType(
                        BillingClient.ProductType.INAPP
                    )
                    .build()
            )
        )
        .build()
        
    billingClient.queryProductDetailsAsync(
        queryProductDetailsParams
    ) { billingResult, productDetailsList ->
        if (!productDetailsList.isEmpty()) {
            productDetails = productDetailsList[0]
            runOnUiThread {
                binding.statusText.text = productDetails.getName()
            }
        } else {
            Log.i(TAG, "onProductDetailsResponse: No products")
        }
    }
}

Much of the code used here should be familiar from the previous chapter. The listener code checks that at least one product was found that matches the query criteria. The ProductDetails object is then extracted from the first matching product, stored in the productDetails variable, and the product name property is displayed on the TextView.

One point of note is that when we display the product name on the status TextView we do so by calling runOnUiThread(). This is necessary because the listener is not running on the main thread so cannot safely make direct changes to the user interface. The runOnUiThread() method provides a quick and convenient way to execute code on the main thread without having to use coroutines.

Launching the Purchase Flow

When the user clicks the buy button, a method named makePurchase() will be called to start the purchase process. We can now add this method as follows:

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

.
.
import android.view.View
.
.
fun makePurchase(view: View?) {
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(
            ImmutableList.of(
                ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .build()
            )
        )
        .build()
    billingClient.launchBillingFlow(this, billingFlowParams)
}

Handling Purchase Updates

The results of the purchase process will be reported to the app via the PurchaseUpdatedListener that was assigned to the billing client during the initialization phase. Add this handler now as follows:

private val purchasesUpdatedListener =
    PurchasesUpdatedListener { billingResult, purchases ->
        if (billingResult.responseCode ==
            BillingClient.BillingResponseCode.OK
            && purchases != null
        ) {
            for (purchase in purchases) {
                completePurchase(purchase)
            }
        } else if (billingResult.responseCode ==
            BillingClient.BillingResponseCode.USER_CANCELED
        ) {
            Log.i(TAG, "onPurchasesUpdated: Purchase Canceled")
        } else {
            Log.i(TAG, "onPurchasesUpdated: Error")
        }
    }

The handler will output log messages if the user cancels the purchase or another error occurs. A successful purchase, however, results in a call to a method named completePurchase() which is passed the current Purchase object. Add this method as outlined below:

private fun completePurchase(item: Purchase) {
    purchase = item
    if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED)
        runOnUiThread {
            binding.consumeButton.isEnabled = true
            binding.statusText.text = "Purchase Complete"
    }
}

This method stores the purchase before verifying that the product has indeed been purchased and that payment is not still pending. The “consume” button is then enabled and the user is notified that the purchase was successful.

Consuming the Product

With the user now able to click on the “consume” button, the next step is to make sure the product is consumed so that only one click can be performed before another button click is purchased. This requires that we now write the consumePurchase() method:

.
.
import kotlinx.coroutines.*
.
.
class MainActivity : AppCompatActivity() {
 
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
.
.
    fun consumePurchase(view: View?) {
 
        val consumeParams = ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()
 
         coroutineScope.launch {
             val result = billingClient.consumePurchase(consumeParams)
 
             if (result.billingResult.responseCode == 
                      BillingClient.BillingResponseCode.OK) {
                 runOnUiThread() {
                     binding.consumeButton.isEnabled = false
                     binding.statusText.text = "Purchase consumed"
                 }
             }
         }
    }
.
.

This method creates a ConsumeParams instance and configures it with the purchase token for the current purchase (obtained from the Purchase object previously saved in the completePurchase() method). This is passed to the consumePurchase() method which is launched within a coroutine using the IO dispatcher. If the product is successfully consumed, code is executed in the main thread to disable the consume button and to update the status text.

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Restoring a Previous Purchase

With the code added so far, we can purchase a product and consume it within a single session. If we were to make a purchase and then exit the app before consuming it the purchase would currently be lost when the app restarts. We can solve this problem by configuring a QueryPurchasesParams instance to search for the unconsumed In-App product and passing it to the queryPurchasesAsync() method of the billing client together with a reference to a listener that will be called with the results. Add a new function and the listener to the MainActivity.kt file as follows:

private fun reloadPurchase() {
 
    val queryPurchasesParams = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.INAPP)
        .build()
 
    billingClient.queryPurchasesAsync(
        queryPurchasesParams,
            purchasesListener
    )
}

private val purchasesListener =
    PurchasesResponseListener { billingResult, purchases ->
        if (!purchases.isEmpty()) {
            purchase = purchases.first()
            binding.consumeButton.isEnabled = true
        } else {
            binding.consumeButton.isEnabled = false
        }
    }

If the list of purchases passed to the listener is not empty, the first purchase in the list is assigned to the purchase variable, and the consume button enabled (in a more complete implementation code should be added to check this is the correct product by comparing the product id and to handle the return of multiple purchases). If no purchases are found, the consume button is disabled until another purchase is made. All that remains is to call our new reloadPurchase() method during the billing setup process as follows:

private fun billingSetup() {
.
.
            if (billingResult.responseCode ==
                BillingClient.BillingResponseCode.OK
            ) {
                Log.i(TAG, "OnBillingSetupFinish connected")
                queryProduct(demo_product)
                reloadPurchase()
            } else {
                Log.i(TAG, "OnBillingSetupFinish failed")
            }
.
.
}

Testing the App

Before we can test the app we need to upload this latest version to the Play Console. As we already have version 1 uploaded, we first need to increase the version number in the build.gradle (Module: InAppPurchase.app) file:

.
.
defaultConfig {
    applicationId "com.ebookfrenzy.inapppurchase"
    minSdk 26
    targetSdk 32
    versionCode 2
    versionName "2.0"
.
.

Sync the build configuration, then follow the steps in the “Creating, Testing and Uploading an Android App Bundle” chapter to generate a new app bundle, upload it to the internal test track and roll it out to the testers. Using the testing track link, install the app on a device or emulator on which one of the test accounts is signed in.

After the app starts it should, after a short delay, display the product name on the TextView. Clicking the buy button will begin the purchase flow as shown in Figure 84-6:

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 1-6

Tap the buy button to complete the purchase using the test card and wait for the Consume Purchase button to be enabled. Before tapping this button, attempt to purchase the product again and verify that it is not possible to do so because you already own the product.

Tap the Consume Purchase button and wait for the “Purchase consumed” message to appear on the TextView. With the product consumed, it should now be possible to purchase it again. Make another purchase, then terminate and restart the app. The app should locate the previous unconsumed purchase and enable the consume button.

Troubleshooting

If you encounter problems with the purchase, make sure the device is attached to Android Studio, either via a USB cable or WiFi, and select it from within the Logcat panel. Enter InAppPurchaseTag into the Logcat search bar and check the diagnostic output, adding additional Log calls in the code if necessary. For additional information about failures, a useful trick is to access the debug message from BillingResult instances, for example:

.
.
} else if (billingResult.responseCode ==
    BillingClient.BillingResponseCode.USER_CANCELED
) {
    Log.i(TAG, "onPurchasesUpdated: Purchase Canceled")
} else {
    Log.i(TAG, billingResult.getDebugMessage())
}

Note that as long as you leave the app version number unchanged in the module-level build.gradle file, you should now be able to run modified versions of the app directly on the device or emulator without having to re-bundle and upload it to the console.

 

You are reading a sample chapter from Android Studio Dolphin Edition – Kotlin Edition.

Buy the full book now in eBook (PDF, ePub, and Kindle) or Print format.

The full book contains 93 chapters and over 820 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

If the test payment card is not listed, make sure the user account on the device has been added to the license testers list. If the app is running on a physical device, try running it on an emulator. If all else fails, you can enter a valid payment method to make test purchases, and then refund yourself using the Order management screen accessible from the Play Console home page.

Summary

In this chapter, we created a project that demonstrated how to add an in-app product to an Android app. This included the creation of the product within the Google Play Console and the writing of code to initialize and connect to the billing client, querying of available products, and, finally, the purchase and consumption of the product. We also explained how to add license testers with the Play Console so that purchases can be made during testing without spending money.