App monetisation and In-App Billing in Android.
import inAppProducts from “./images/billing/in-app-products.png”; import addProducts from “./images/billing/add-product.png”;
Android apps and monetisation.
Some devs make apps for fun, some make them for learning things. But if you’ve got a good app idea, chances are you want to be able to monetise it. With Android, there are several ways of going about it i.e. putting in ads - which relies on not only high volumes of downloads but large amounts of ad screen time - or you could sell a paid app - but that’s not without its challenges. A lot of mobile users will want to be able to try your app just so they can work out if it’s worth their while. This would perhaps mean publishing a trial version of your app and then subtly diverting these users to a paid version - but this does mean you’d have to then publish two versions of your app - however, you’re still splitting your audience across two apps - this means splitting your ratings, downloads and app indexation on the Play store!
Best of both worlds.
There is a better way. You can combine these two monetisation models by building a single version of your app and introducing paid features in your app - by using the Android’s In-App Billing API - and its super easy to implement. There are a couple of things you’ll want to work out first though - for example whether you want to charge your users with a subscription model i.e. something a media service would charge its users every month or over some other pre-determined amount of time, or have a consumable feature - i.e. something like ammo in a game - but in this particular example we’re going to be focussing on having a perpetually unlockable feature which really is a baseline implementation of the lot.
Let’s give it a go.
The BillingClient
convenience class provides a gateway into the Play Billing Library and abstracts away calls to the In-App billing API for us. Here’s how to leverage it:
First we need to get an
instance
of theBillingClient
. There is a convenienceBuilder
class that requires the calling Activity’sContext
. We can also set thePurchasesUpdatedListener
callback here which is important because we want to be notified of when the billing flow is complete and the user has been charged of course!1 2 3
mBillingClient = BillingClient.newBuilder(mContext) .setListener(this) .build();
Let’s take a quick detour and also update our permissions to use
In-App Billing
in ourAndroidManifest.xml
file.1
<uses-permission android:name="com.android.vending.BILLING" />
We set up the
PurchasesUpdatedListener
previously - so, when theonPurchasesUpdated
callback gets notified - there are a couple of things to check. Firstly, there is an integer valueresponseCode
. You can read about what responseCode means but for now, we’re looking forBillingResponse.OK
. Secondly, a@nullable List<Purchase>
value - we can iterate over all of its values and look for particularskus
- more on this later. BecauseList<Purchas>
is nullable, its always a good idea to guard it with a null check of course and then iterate over to see if the requiredsku
has been returned.1 2 3 4 5 6 7
if (purchases.size() > 0) { for (Purchase result : purchases) { if (result.getSku().equals(mContext.getResources().getString(skuOfInterest))) { //the billing flow has completed successfully. } } }
Sweet. Now, we’re Ok to go ahead and connect up our
mBillingClient
, we’ve previously built and added a callback to. We are required to pass in aBillingClientStateListener
. Basically, this callback comes back with a integer valueBillingResponse
code which helps us determine if connnectivity was successful or not. There is also aonBillingServiceDisconnected
method which is needed to be queried and consequently we need to return some meaningful feedback error to the user if required.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mBillingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(int responseCode) { if (responseCode == BillingClient.BillingResponse.OK) { mIsBillingClientConnected = true; //do other interesting things here - as we're connected now. } } @Override public void onBillingServiceDisconnected() { mIsBillingClientConnected = false; //handle this failure gracefully. } }
now we know that we can query the In-App billing API when
mIsBillingClientConnected
istrue
and gracefully try either reconnecting or show an error response of sorts when it isfalse
.Now we’ve mentioned
sku
s a few times in previous steps. This is just aString
value that an In-App product gets given when we generate one. Let’s explore this here - we’ll add an In-app product that we can bill against.On the Play Console, under Store Presence, there is an option to create In-App products click on Add a Managed Product - then give it an id (only underscores, lowercase letters and digits allowed!) - which will become the
sku
for your In-App ProductNext, give it a title and a description as well as price. This will show up when a user tries to buy your In-app product through your app, so make it short and simple. The Play console does automatic currency conversions for countries your app is supported in - so if you need to tweak that for individual countries - you can it here as well. Finally, click on save and it will add your
In-App product
.<img src= { inAppProducts } width="30%" />
<img src= { addProducts } width="50%" />
Now that you’ve got an
sku
, you can create a@StringRes
for it.1
<string name="id_sku_my_product">01_my_first_product</string>
Nearly there! Now you can query for if the user has purchased your In-App Product. This is determined by running the
queryPurchaseHistoryAsync
callback on yourmBillingClient
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
mBillingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() { @Override public void onPurchaseHistoryResponse(@BillingClient.BillingResponse int responseCode, List<Purchase> purchasesList) { if (responseCode == BillingClient.BillingResponse.OK) { if (purchasesList != null) { if (purchasesList.size() > 0) { for (Purchase result : purchasesList) { if (result.getSku().equals(mContext.getResources().getString(R.string.01_my_first_product))) { //success! your user has bought your In-App product. Woohoo! } } } } } } });
Although, Android gives you some more convenience on top of this. You do not need to call
queryPurchaseHistoryAsync
everytime. Purchases are cached by the Play store app - so you can just runqueryPurchases
. So, what naturally follows is that you would use this call to see if it comes back with legitPurchaseResult
, if not you can runqueryPurchaseHistoryAsync
.1 2 3 4 5 6 7 8 9 10 11 12
Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); if (purchasesResult != null) { if (purchasesResult.getPurchasesList() != null) { if (purchasesResult.getPurchasesList().size() > 0) { for (Purchase result : purchasesResult.getPurchasesList()) { if (result.getSku().equals(mContext.getResources().getString(R.string.01_my_first_product))) { //success! your user has bought your In-App product. Wowie! } } } } }
Ok, so that’s the basic flow. Let’s recap:
- Build the
BillingClient
. - Connect said
BillingClient
. - On success, pass in the
purchasesUpdatedListener
callback. - When purchases are updated query for your
sku
by running first runningqueryPurchases
orqueryPurchaseHistoryAsync
.
But what about users that have already bought your product? Well, you’ll have to run this process at the start of your app too! Users that have already bought your app don’t need to be run through a purchase process of course!
- Build the
Optimisation tips.
It almost seems like you’re going to have to follow the billing process twice. You’re just better off using a
Singleton
instance of theBillingClient
. If you’re using a DI framework like the excellent Dagger, you’re better off creating aSingleton
component on your dependency graph and then injecting it as needed.One of the biggest challenges for me was the fact that
In-App Billing
was not showing up on the Play console. That’d be because my production APK didn’t have the In-App billing permissions. This is easily remedied by creating an Alpha release of your app - and actually its highly recommended because you can then use test accounts to test the purchase flows. As long as your test users are added as alpha testers for your app!