Getting Started With In-App Purchases

Learn how to get started with in-app purchases and implement this library inside your next project. By Mattia Ferigutti.

See course reviews 4 (1) · 1 Review

Download materials
Save for later
Share

Learn how to get started with in-app purchases and implement this library inside your next project.

When Android was born, the only way to pay was directly on the Google Play Store. This method wasn’t ideal since no one wants to buy something without seeing it.

To solve this issue, developers started using another solution, in-app purchases. With in-app purchases, users can download your app for free from the play store and decide whether to buy something to unlock new features, such as a new sword from League of Legends. This business model, freemium, is the most used revenue technique among developers.

It’s difficult to handle money transactions on your own while keeping the system as secure as possible. So, Android introduced the Google Play Billing Library. It’s powerful enough to convince users to commit to a service, use subscriptions and make in-app purchases with consumable and non-consumable products.

In this tutorial, you’ll learn how to implement this library and about:

  • Setting up the Google Play Store.
  • Non-consumable purchases.
  • Purchase flow.
  • Best practices.
  • Setting up testing accounts.
Note: To follow along with this tutorial, you need experience with the Android SDK, Kotlin Flow and StateFlow, Coroutines and Google Play Developer Console.

Getting Started

Download the materials by clicking Download Materials at the top or bottom of this tutorial. Then, open the starter project in Android Studio.

The app you’ll build is a monster catalog with two screens.

Monsters home screen

The first screen shows the most powerful monster you have.

Monsters catalogue screen

Users can choose which monster they want to buy from a list on the second screen. Every monster has different power points. In this case, the more expensive, the better.

Project structure

Take a look at the project’s structure:

  • MonsterAdapter.kt contains the logic to display a list of monsters.
  • BaseApplication.kt contains an instance of the BillingHelper class which can be shared among all the activities.
  • BillingHelper.kt contains the logic to handle in-app purchases.
  • Security.kt is a utility class that checks the validity of the public key from the Google Play Developer Console.
  • MonsterData.kt contains the list of monsters.

This article has three different sections:

  1. Learning the service fees, terminology and concepts of the library.
  2. Implementing the logic inside a wrapper class.
  3. Implementing the just created class inside your app.

You’ll start by learning a little more about the service fees.

Service Fees

Unfortunately, Google doesn’t give you access to this library for free. But the good news is that as of July 1, 2021, the fees have decreased from 30% to 15%. However, there are a few conditions you need to respect:

  • If enrolled in the 15% service fee tier:
    • The fee is 15% for the first $1M (USD) of earnings each year.
    • The fee increases to 30% for earnings in excess of $1M (USD) each year.
  • For developers not enrolled in the 15% service fee tier, the service fee is 30%.
  • For subscription products purchased by subscribers you retain after 12 paid months, the service fee is 15%.

Terminology

Before implementing the library, you need to get familiar with Google’s terminology.

Content Types

You can use the Google Play Billing Library to sell digital products and content in your Android app. This library can help you sell the following types of digital content: one-time products and subscriptions. One-time products fit into two subcategories:

  • A consumable product is one that a user consumes to receive in-app content. For example, many games let you buy coins you can spend to enhance your character. When a user consumes the product, your app dispenses the associated content, and the item is available for purchase again.
  • A non-consumable product is a product users purchase only once to provide a permanent benefit. Examples include apps that let you remove ads after making the payment.

This article will walk you through only the one-time products. However, the steps are the same for a subscription.

Concepts

You also need to get familiar with some key concepts:

  • A flow describes the steps required to buy a digital product.
  • A product SKU is the ID of a specific product type. Every item at your favorite grocery store has a barcode, usually on the back of the product. This barcode serves as the product’s identity, so every time the cashier passes the product over the scanner, they know the exact price of the item.
  • A purchase token is a string that indicates that a Google user paid for a specific product.
  • An order ID is a string that represents a financial transaction on Google Play. Every time a financial transaction occurs, Google Play creates an order ID which it includes on the receipt emailed to the buyer. You can also use the order ID to manage refunds in the order management section of the Google Play Console. Google Play also uses order IDs in sales and payout reports.

With that information in hand, it’s time to get started.

Setting-Up the Google Play Console

You’ll use the Google Play Console throughout this tutorial. If you haven’t created an account yet, it’s time to do so. Make sure to follow all the steps in order.

Open the Google Play Console and open All apps. Click Create app in the top-right corner. Give a name to your app and fill out all the other fields.

Then, on the Free or paid field, select Free. Paid means you need to pay directly on the play store before downloading the app. Finally, click Create app.

Now, you’ll see your app’s dashboard. You need the app’s public key, a base-64 string. To find it, scroll the menu on the left until you find Monetization setup. Click it and copy the base64-encoded RSA public key under Licensing.

Next, you’ll set up the Android project.

Setting-Up the Android Project

Now that you have the app key, open Constants.kt inside your starter project. You’ll see this:

// PUBLIC API KEY
const val PUBLIC_KEY = "YOUR_KEY"

Replace YOUR_KEY with the actual key you copied for the Play Console.

Then open build.gradle. Add this dependency and sync the project:

dependencies {

  // Google Play Billing Library
  def billing_version = "4.0.0"
  implementation "com.android.billingclient:billing-ktx:$billing_version"
}

With your Android project ready, it’s time to take a look at the APK.

Loading the APK in the Play Console

If you aren’t using the project sample provided in this article, it’s advisable to create a new project and copy all the code and resources appropriately.

To connect the app to the in-app purchases defined in the Play Console, you must load the signed APK, or Android App Bundle, inside the Play Console. For instructions, check out this great article.

Back to the Google Play Developer Console

Now that you built your APK, you can upload it on the Play Console. This part is critical, so follow all the steps carefully.

First, go to All apps. Select Setup and License testing from the menu on the left. Insert all the accounts that can test your apps: These accounts must be associated with Google accounts used in the Play Store.

Then complete all the steps inside LET US KNOW ABOUT THE CONTENT OF YOUR APP. You don’t need to complete the MANAGE HOW YOUR APP IS ORGANIZED AND PRESENTED since you’re just testing the API and the app doesn’t need a page on the Play Store.

Screenshot of the steps required to provide information about your app

Now select Internal testing from the menu on the left. Click Create new release. Upload your app and click Save.

Don’t publish it yet. You’re using Internal Testing because it’s the only one that lets you publish an app without the need to have an app preview.

Then select In-app products from the menu on the left. Click Create product. Set the Product ID, Name, Description and Default price.

You won’t be able to change the Product ID once you assigned it. For the Monsters app, you need to set three different product IDs and their relative prices. Define monster_level_2 as 2 dollars, monster_level_3 as 3 dollars and monster_level_4 as 4 dollars.

Remember to Save and Activate the product.

Screenshot of the Product IDs

Now, go back to Internal testing. Click Testers and Create email list. Every account in this list is eligible to test your app.

Give the list a name, add all the emails and Save changes. Now check the box with your testers list. Scroll down the page and copy the link by clicking Copy link.

Screenshot of the list of testers

Finally, publish the app. Click the Edit release button at the top-right. Then click Reviews release and finally select Start rollout to Internal testing.

Send the copied link to your testers. They need to accept the invite to use in-app purchases. They can also download the app directly from the Google Play Store through this link.

It doesn’t matter if you’re testing the app on your device or emulator. Even if they install the app from Android Studio, they still need to accept the invite. After doing so, they’ll see something like this:

Screenshot of the accepted invite

Good job! Now you’re ready to implement the code to manage the in-app purchases.

Handling Purchases

Open BillingHelper.kt. As you can see, the constructor of the class takes four parameters.

class BillingHelper private constructor(
    application: Application,
    private val defaultScope: CoroutineScope,
    knownInAppSKUs: Array<String>?,
    knowConsumableInAppKUSs: Array<String>?
)

Breaking down the parameters:

  • defaultScope helps execute tasks in background. As you may have noticed, the library you implemented has the ktx abbreviation. This means it supports Kotlin features including coroutines that you’ll use in this tutorial.
  • knownInAppSKUs is a list that contains all the SKUs for non-consumable products.
  • knowConsumableInAppKUSs is a list that contains all the SKUs for consumable products.

This class also implements the Singleton Pattern, which means you can only have one single instance of this class. Having multiple instances, in this case, can lead to errors. For example, imagine if different instances tried to consume the same purchase.

Connecting the Billing Client

It’s time to write some code. Implement PurchasesUpdatedListener and BillingClientStateListener in the class and add all their methods:

override fun onPurchasesUpdated(
  billingResult: BillingResult, 
  list: MutableList<Purchase>?
) {
  
}

override fun onBillingSetupFinished(billingResult: BillingResult) {
  
}

override fun onBillingServiceDisconnected() {
  
}

Every method responds to a specific event:

  • You call onPurchasesUpdated when new purchases occur. This happens in response to a billing flow. You’ll learn about this flow later.
  • onBillingSetupFinished is called to notify you that setup is complete.
  • onBillingServiceDisconnected isn’t usually called and happens primarily if the Google Play Store self-upgrades or is force closed. If this happens, you must reconnect to the billing service.

Once you define the interfaces, initialize the BillingClient inside the init block:

init {

    //Connecting the billing client
    billingClient = BillingClient.newBuilder(application)
        .setListener(this)
        .enablePendingPurchases()
        .build()
    billingClient.startConnection(this)
}

Here’s a code breakdown:

  • Here, you implement enablePendingPurchases() which means you ensure entitlement only once you secure payment. For example, in some countries you can pay for in-app purchases or subscriptions using cash in places authorized by Google. So you need to acknowledge only when you receive the payment.
  • After creating a BillingClient, you need to establish a connection to Google Play by calling startConnection(). The connection process is asynchronous.

Implementing onBillingSetupFinished

When the setup is complete, the BillingClient calls onBillingSetupFinished. You need to check whether the response code from BillingClient has a positive response.

Implement the following code:

override fun onBillingSetupFinished(billingResult: BillingResult) {
    val responseCode = billingResult.responseCode
    val debugMessage = billingResult.debugMessage
    Log.d(TAG, "onBillingSetupFinished: $responseCode $debugMessage")
    when (responseCode) {
      BillingClient.BillingResponseCode.OK -> {
        defaultScope.launch {
          querySkuDetailsAsync()
          restorePurchases()
        }
      }
    }
  }

In this code, you check if responseCode is equal to BillingClient.BillingResponseCode.OK. By doing so, you know that the billing client is ready for you to query purchases, but that doesn’t mean your app is set up correctly in the console. It tells that you have a connection to the Billing service.

You can use the menu on your left to jump to the querySkuDetailsAsync() and restorePurchases() methods in this tutorial.

Adding Flow

You initialized the BillingClient, but you also need to add a purchase flow for every SKU to complete the setup. Each SKU has a different flow which gives you information about the product and notifies you if an event happens.

Inside the BillingHelper class, define addSkuFlows():

private enum class SkuState {
    SKU_STATE_UNPURCHASED, 
    SKU_STATE_PENDING, 
    SKU_STATE_PURCHASED, 
    SKU_STATE_PURCHASED_AND_ACKNOWLEDGED
}

private fun addSkuFlows(skuList: List<String>?) {
    if (null == skuList) {
      Log.e(
        TAG,
        "addSkuFlows: " +
         "SkuList is either null or empty."
      )
    }
    for (sku in skuList!!) {
      val skuState = MutableStateFlow(SkuState.SKU_STATE_UNPURCHASED)
      val details = MutableStateFlow<SkuDetails?>(null)
      // this initialization calls querySkuDetailsAsync() when the first 
      //  subscriber appears
      details.subscriptionCount.map { count ->
        count > 0
      } // map count into active/inactive flag
          .distinctUntilChanged() 
          .onEach { isActive -> // configure an action
            if (isActive) {
              querySkuDetailsAsync()
            }
          }
          .launchIn(defaultScope) // launch it inside defaultScope

      skuStateMap[sku] = skuState
      skuDetailsMap[sku] = details
    }
}

Here’s a code breakdown:

  • The function takes a list of SKUs as a parameter. It defines a skuState and some details for every SKU. Both instances use the MutableStateFlow class since they only need to hold the latest state of the SKU.
  • SkuState is an enum that defines the states a SKU can have throughout the flow.
  • details implements some operators to manage the data. You use subscriptionCount through the map operator to check if it has any subscribers. distinctUntilChanged() verifies that the new object isn’t the same as the previous one. In this case, it reacts on true and false changes.

    onEach asks for details for each SKU by calling querySkuDetailsAsync(). Finally, it launches this process inside the defaultScope which is a CoroutinesScope passed as parameter of the class.

  • You save both details and skuState inside a map shared with the entire class. This process makies it simple to keep track of all the SKUs.

Now, call this method inside the init block:

init {
    //Add flow for in app purchases
    addSkuFlows(this.knownInAppSKUs)
}

Implementing querySkuDetailsAsync

Now you need to query the SKUs’ information. You’ll interact with the Play Console by asking for the SKUs’ details. SkuDetails are critical since they share information about the item names and price lists with the user and are required to make a purchase.

Inside the BillingHelper class, implement these lines of code:

private suspend fun querySkuDetailsAsync() {
    if (!knownInAppSKUs.isNullOrEmpty()) {
      val skuDetailsResult = billingClient.querySkuDetails(
        SkuDetailsParams.newBuilder()
          .setType(BillingClient.SkuType.INAPP)
          .setSkusList(knownInAppSKUs.toMutableList())
          .build()
      )
      // Process the result
      onSkuDetailsResponse(
        skuDetailsResult.billingResult, 
        skuDetailsResult.skuDetailsList
      )
    }
  }

Notice that the method is suspend. That’s because at its core querySkuDetails() uses coroutines to get data from the network. The method then:

  • Sets the type to BillingClient.SkuType.INAPP with setType(). You query in-app purchases only. Choose BillingClient.SkuType.SUBS if you’re using subscriptions.
  • Using setSkusList(), it passes a list of SKUs you want to get information from.
  • Then it processes the result calling onSkuDetailsResponse(). The result is a List of SkuDetails objects.

Implementing restorePurchases

Restoring the previous purchases is essential, and you should call this method every time the activity or fragment starts. Add the following snippet of code:

private suspend fun restorePurchases() {
    val purchasesResult = 
      billingClient
        .queryPurchasesAsync(BillingClient.SkuType.INAPP)
    val billingResult = purchasesResult.billingResult
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
      handlePurchase(purchasesResult.purchasesList)
    }
  }

This method calls all the SKUs available and checks if they have been purchased. It simply:

  • Calls queryPurchasesAsync() and returns a PurchasesResult object. Notice that, as before, this method is suspend as it also uses coroutines under the hood.
  • Checks whether the responseCode is valid and passes the result to handlePurchase().

Implementing onSkuDetailsResponse

Once querySkuDetailsAsync() gets the result, it passes it to onSkuDetailsRsponse to elaborate on the response. It emits the details for every SKU.

Inside the BillingHelper class, implement the following code:

private fun onSkuDetailsResponse(
  billingResult: BillingResult, 
  skuDetailsList: List<SkuDetails>?
) {
    val responseCode = billingResult.responseCode
    val debugMessage = billingResult.debugMessage
    when (responseCode) {
      BillingClient.BillingResponseCode.OK -> {
        Log.i(TAG, "onSkuDetailsResponse: $responseCode $debugMessage")
        if (skuDetailsList == null || skuDetailsList.isEmpty()) {
          Log.e(
            TAG,
            "onSkuDetailsResponse: " +
              "Found null or empty SkuDetails. " +
              "Check to see if the SKUs you requested are correctly" +
              " published in the Google Play Console."
          )
        } else {
          for (skuDetails in skuDetailsList) {
            val sku = skuDetails.sku
            val detailsMutableFlow = skuDetailsMap[sku]
            detailsMutableFlow?.tryEmit(skuDetails) ?: 
              Log.e(TAG, "Unknown sku: $sku")
          }
        }
      }
    }
  }

Here’s how the function works:

  • First, it checks if the response is positive and the skuDetailsList is neither null nor empty.
  • detailsMutableFlow gets the specific MutableStateFlow object defined inside skuDetailsMap, which is a Map that keeps a reference to all the skuDetails. Whenever you need a skuDetails you can use its SKU to get it. You first define these objects inside addSkuFlows().
  • Using tryEmit(), you emit the value and all the collectors are able to get it. tryEmit() is different from emit() since it tries to emit a value without suspending. In fact, you aren’t calling it inside a coroutine.

Implementing launchBillingFlow

Finally, you can let the user buy a product! But first, you need to show them a purchase screen and let the user pay for the item.

Inside the BillingHelper class, write this code inside your project:

fun launchBillingFlow(activity: Activity, sku: String) {
    val skuDetails = skuDetailsMap[sku]?.value
    if (null != skuDetails) {
      val flowParams = BillingFlowParams.newBuilder()
          .setSkuDetails(skuDetails)
          .build()
      billingClient.launchBillingFlow(activity, flowParams)
    }
    Log.e(TAG, "SkuDetails not found for: $sku")
  }

Here, you create the instance of the class BillingFlowParams using BillingFlowParams.Builder and pass the skuDetails inside setSkuDetails(). Then, you call launchBillingFlow() which is responsible for showing the bottom purchase screen inside your app.

You’ll see this screen call launchBillingFlow():

Screenshot of the app when you call launchBillingFlow

Implementing onPurchasesUpdated

After calling launchBillingFlow(), you need to handle the response. For example, the user might buy something within the app or initiate a purchase from Google Play Store.

Copy and paste the code inside the BillingHelper class:

override fun onPurchasesUpdated(billingResult: BillingResult, list: MutableList<Purchase>?) {
    when (billingResult.responseCode) {
      BillingClient.BillingResponseCode.OK -> if (null != list) {
        handlePurchase(list)
        return
      } else Log.d(TAG, "Null Purchase List Returned from OK response!")
      BillingClient.BillingResponseCode.USER_CANCELED -> 
        Log.i(TAG, "onPurchasesUpdated: User canceled the purchase")
      BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> 
        Log.i(TAG, "onPurchasesUpdated: The user already owns this item")
      BillingClient.BillingResponseCode.DEVELOPER_ERROR -> Log.e(
        TAG,
        "onPurchasesUpdated: Developer error means that Google Play " +
          "does not recognize the configuration. If you are just " +
          "getting started, make sure you have configured the " +
          "application correctly in the Google Play Console. " +
          "The SKU product ID must match and the APK" +
          "you are using must be signed with release keys."
      )
      else -> 
        Log.d(
          TAG,
          "BillingResult [" + billingResult.responseCode + "]: " + 
          billingResult.debugMessage
        )
    }
  }

Google Play calls onPurchasesUpdated() to deliver the result of the purchase operation launched previously with launchBillingFlow(). This method gets notifications for purchase updates.

All purchases reported here must either be consumed or acknowledged. Failure to either consume or acknowledge will result in a refund. You’ll see this concept better later.

The code is pretty straightforward:

  • You check that the response is okay with BillingClient.BillingResponseCode.OK so you have a green light to proceed with handling the purchase.
  • In case the code gives you an error, you print it in the console.

Implementing handlePurchase

handlePurchase is the most challenging method to understand. But hang tight. You’ve almost made it!

Now that you have all your purchases, it’s time to validate them. Add this code inside your BillingHelper class:

private fun handlePurchase(purchases: List<Purchase>?) {
    if (null != purchases) {
      for (purchase in purchases) {
        // Global check to make sure all purchases are signed correctly.
        // This check is best performed on your server.
        val purchaseState = purchase.purchaseState
        if (purchaseState == Purchase.PurchaseState.PURCHASED) {
          if (!isSignatureValid(purchase)) {
            Log.e(
              TAG, 
              "Invalid signature. Check to make sure your " + 
                "public key is correct."
            )
            continue
          }
          // only set the purchased state after we've validated the signature.
          setSkuStateFromPurchase(purchase)

          if (!purchase.isAcknowledged) {
            defaultScope.launch {
              for (sku in purchase.skus) {
                // Acknowledge item and change its state
                val billingResult = billingClient.acknowledgePurchase(
                    AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.purchaseToken)
                        .build()
                )
                if (billingResult.responseCode != 
                  BillingClient.BillingResponseCode.OK) {
                    Log.e(
                      TAG, 
                      "Error acknowledging purchase: ${purchase.skus}"
                    )
                } else {
                  // purchase acknowledged
                  val skuStateFlow = skuStateMap[sku]
                  skuStateFlow?.tryEmit(
                    SkuState.SKU_STATE_PURCHASED_AND_ACKNOWLEDGED
                  )
                }
              }
            }
          }
        } else {
          // purchase not purchased
          setSkuStateFromPurchase(purchase)
        }
      }
    } else {
      Log.d(TAG, "Empty purchase list.")
    }
  }

Here’s a code breakdown:

  • A purchase won’t be acknowledged if it has a purchaseState that is not equal to PurchaseState.PURCHASED or doesn’t have a valid signature. isSignatureValid() uses the class Security to check the validity of your purchase using the public key of your Play Console. However, you should use your own server to verify the validity of a purchase.

    For more information, Google wrote a good article about this topic.

  • After validating the signature, call setSkuStateFromPurchase() to set the state of the purchase.
  • Now you check if the app acknowledged the purchase. If it wasn’t acknowledged, you need to do so. You can acknowledge a purchase using acknowledgePurchase().

    This function is suspend so you need to call it inside a coroutine. Pass the purchase.purchaseToken inside setPurchaseToken() to acknowledge the product.

  • If the app could acknowledge the purchase, it will notify all the collectors of this event.

What does acknowledging a purchase mean? It’s like saying, “I’m sure of the legitimacy of this payment. I entitle you, user, as the receiver of my product, giving you all the features you paid for”.

Once the user purchases a product, you must acknowledge it within three days, or Google Play will automatically refund and revoke the purchase.

Implement setSkuStateFromPurchase

When a purchase changes, you need to update its state. Inside the BillingHelper class, implement the following code:

private fun setSkuStateFromPurchase(purchase: Purchase) {
    if (purchase.skus.isNullOrEmpty()) {
      Log.e(TAG, "Empty list of skus")
      return
    }

    for (sku in purchase.skus) {
      val skuState = skuStateMap[sku]
      if (null == skuState) {
        Log.e(
          TAG, 
          "Unknown SKU " + sku + ". Check to make " + 
            "sure SKU matches SKUS in the Play developer console."
        )
        continue
      }

      when (purchase.purchaseState) {
        Purchase.PurchaseState.PENDING -> 
          skuState.tryEmit(SkuState.SKU_STATE_PENDING)
        Purchase.PurchaseState.UNSPECIFIED_STATE -> 
          skuState.tryEmit(SkuState.SKU_STATE_UNPURCHASED)
        Purchase.PurchaseState.PURCHASED -> if (purchase.isAcknowledged) {
          skuState.tryEmit(SkuState.SKU_STATE_PURCHASED_AND_ACKNOWLEDGED)
        } else {
          skuState.tryEmit(SkuState.SKU_STATE_PURCHASED)
        }
        else -> 
          Log.e(
            TAG, 
            "Purchase in unknown state: " + purchase.purchaseState
          )
      }
    }
  }

This function only emits the new state of purchase through a StateFlow.

Getting Product Details

Finally, you have everything set up. But you need methods to get details about the price, title, and purchase description. Write this code inside the BillingHelper class:

/**
   * The title of our SKU from SkuDetails.
   * @param SKU to get the title from
   * @return title of the requested SKU as an observable
   * */
  fun getSkuTitle(sku: String): Flow<String> {
    val skuDetailsFlow = skuDetailsMap[sku]!!
    return skuDetailsFlow.mapNotNull { skuDetails ->
      skuDetails?.title
    }
  }

  fun getSkuPrice(sku: String): Flow<String> {
    val skuDetailsFlow = skuDetailsMap[sku]!!
    return skuDetailsFlow.mapNotNull { skuDetails ->
      skuDetails?.price
    }
  }

  fun getSkuDescription(sku: String): Flow<String> {
    val skuDetailsFlow = skuDetailsMap[sku]!!
    return skuDetailsFlow.mapNotNull { skuDetails ->
      skuDetails?.description
    }
  }

You take the correct skuDetailsFlow based on the given sku. Then mapNotNull transforms skuDetailsFlow in a String and returns its title using skuDetails?.title.

Implementing BillingHelper Inside The Project

Finally, BillingHelper.kt is complete! Now you can implement this class inside your project.

Open MonsterFragment.kt and replace getMonster() with:

fun getMonster() : LiveData<Monster> {
    val monsters = MonsterData.getListOfMonsters(requireContext())

    // combine 3 flows inside one
    val monster = combine(
        billingHelper.isPurchased(MONSTER_LEVEL_2),
        billingHelper.isPurchased(MONSTER_LEVEL_3),
        billingHelper.isPurchased(MONSTER_LEVEL_4)
    ) { level2, level3, level4 ->
      when {
        level4 -> {
          return@combine monsters[3]
        }
        level3 -> {
          return@combine monsters[2]
        }
        level2 -> {
          return@combine monsters[1]
        }
        else -> {
          return@combine monsters[0]
        }
      }
    }.asLiveData()

    return monster
  }

combine() waits for all the three flows to complete and then executes the code inside. This code returns the most powerful monster you have.

Open MonsterStoreFragment.kt and define these two methods inside the class:

fun getSkuPrice(sku: String?) : LiveData<String>? {
    if (null == sku) return null
    return billingHelper.getSkuPrice(sku).asLiveData()
  }

  fun makePurchase(sku: String?) {
    if (null != sku) {
      billingHelper.launchBillingFlow(requireActivity(), sku)
    } else {
      Toast.makeText(
        requireContext(), 
        getString(R.string.item_not_available), 
        Toast.LENGTH_SHORT
      ).show()
    }
  }

Here:

  • getSkuPrice() transforms billingHelper.getSkuPrice() from a Flow to a LiveData to be observed inside the MonstersAdapter.
  • makePurchase() simply uses billingHelper.launchBillingFlow() to launch a billing flow.

Now, call makePurchase() inside onItemClickListener of the MonstersAdapter and pass the SKU of the purchase.

onItemClickListener = { purchase ->
  makePurchase(purchase.sku)
}

With that, you’re done!

Where to Go From Here?

While the code in this tutorial isn’t straightforward, it’s handy once you understand it. Flow keeps you updated whenever an event happens.

You finally have a class that can handle payments, but it’s essential to implement it using the best technologies and practices. To do so, you need to know how to create good architecture. Check out this book to learn more about this topic!

If you have any questions or comments, please join the forum below!