Now when your app is rocking the Play Store, has 1m+ downloads and a big community of fans, it’s time to think about monetization. I bet, the first thing you stumble upon while searching for “android in-app billing” is this article from the official Android documentation.
And you should read it.
Seriously, without a deep understanding of APIs provided from the SDK, without knowledge of the base data structures, method signatures and terms used in the documentation, adding in-app purchases to your app is going to be painful and will take a lot of time and effort.
Let’s assume that you have read the article and know the difference between the managed products and the subscriptions. You’re aware that you can’t cancel a subscription from within the app, only downgrade or upgrade it. You know what “consuming” means.
There are, basically, two alternatives: either you implement billing yourself or you use some library for it.
Implementing billing yourself
There is a great guide on Android Developer portal about it. Just follow it.
However, this approach has several disadvantages, such as:
- Once imported the code of Trivial Drive becomes your responsibility. If it changes due to an API change or a bug fix, then you need to update your project. As now they are inseparable.
- Trivial Drive doesn’t provide callbacks for purchase signature verification that is recommended to be performed on a remote server.
- Trivial Drive is not efficient: it establishes one connection to the billing service per IabHelper, creates a new thread each asynchronous request, etc.
- You don’t need to be a genius to understand that Trivial Drive code just smells, see MainActivity and IabHelper, for example.
Have you already changed your mind?
Using a billing library
I’m going to show you how to implement common tasks using the library’s sample app. If you prefer digging in the code yourself here is a range of changes in git that are used in this tutorial: 0d95bf5..54789c3.
Step #0. Preparation
First, we need to import the library into the app’s project. In Gradle it can be done with the following one-liner:
bb989bf Let’s define a custom application class, CheckoutApplication, with a single Billing object. As Android creates Application only once, we’re guaranteed to have only one instance of Billing. It’s important as some operations performed by it shouldn’t be done more than once (for example, binding to the billing service).
If your app uses a DI framework Billing class can be marked as a singleton.
3c57339. Here we introduce the main activity which shows a list of use cases available in the app. The code is straightforward: activity contains an enum (UseCase) that represents all use cases available in the app (empty initially), an adapter and a view holder to show each use case in the list (RecycleView+LinearLayoutManager).
6198659. Get hold of CheckoutApplication through Activity, which is needed to access Billing in the UI code.
Use case #1. Static responses
To test the static responses as described on d.android.com.
3a8966b. StaticActivity creates an instance of Checkout class via Checkout#forActivity call. It is immediately started and then used in the Buy button’s callback. Note also that it is used in two other Activity methods: onDestroy and onActivityResult. The former is needed to free any resources that might be acquired by Checkout while the latter merely notifies Checkout about the purchase result.
Checkout#start initiates connection to the billing service. Checkout#stop — disconnects from it. These two methods should always be used in pair — every time a Checkout instance is started it should be stopped.
76c0ffb. StaticActivity is aware now of what is happening under the hood of the library and logs everything on the screen into the console TextView.
Time to run the code… And it doesn’t work.
It turned out that some static responses are broken. This is not the bug you’re looking for, move along.
Use case #2. Ad banner
To show an ad banner. The banner should be removed if user purchases an ad-free version.
6429f48. This commit starts with declaration of BannerActivity which shows some text and an ad banner. As in the previous example, we need to write some boilerplate code to obtain an instance of Checkout and start Inventory loading. As it might take some time to load the inventory, the banner is hidden at the beginning (we don’t want to show it at all if user has already done the purchase).
Inventory loading consists of 3 steps:
- Preparing a request, for example: Inventory.Request.create().loadAllPurchases()
- Calling Checkout#loadInventory
- Waiting for the result in Inventory.Callback
Good to know that Checkout#onStop cancels all pending requests, including inventory loading. Thus, Inventory.Callback is never called after the enclosing activity is destroyed.
The code of BannerActivity.InventoryCallback is straightforward: if billing is not supported do nothing (no banner is shown in this case); if ad-free option is not purchased — show the banner.
01c48ea. Here we add a menu to the activity with a single “Remove ad banner” entry. By clicking it we start a purchase flow which ends in RequestListener#onSuccess (or in RequestListener#onError if something goes wrong :P). BannerActivity.PurchaseListener just hides the banner in the callback method.
Use case #3. List of SKUs
To show a list of SKUs fetched from Play Store. User can purchase/consume items in the list.
7963597. The main activity in this example is SkusActivity. It shows a list of SKUs that are loaded with the help of Checkout#loadInventory method.
In contrast to the use case #2, where only purchases are loaded, here we also want to load listings from the store. Unfortunately, Android’s billing API doesn’t allow to load all available at the moment items — a list of SKU identifiers is required by the API method that handles such request.
SkusActivity prepares the identifiers list in #getInAppSkus method and uses it in Inventory.Request#loadSkus right before Checkout#loadInventory is called. InventoryCallback updates the adapter when the loading finishes.
e11a495. Next commit makes items in the list clickable. Adapter#onClick calls either SkusActivity#purchase or SkusActivity#consume depending on the current state of the clicked item. At the end of the operation, the list is reloaded via SkusActivity#reloadInventory call.
Use case #4. Managing subscriptions
To show a list of subscriptions and provide user interface for managing them.
9aec6c4. The main activity in this use case, namely SubscriptionsActivity, allows user to:
- Buy subscriptions
- Replace purchased subscriptions (upgrade/downgrade)
As you might have noticed user can’t cancel a purchased subscription but can only convert it to some other subscription. The reason for that is simple — the billing API doesn’t allow subscription cancellation. Until this is changed in the API the only one way of managing subscriptions in the app is by replacing purchased subscriptions.
As in the use case #3, we need some subscription information (title, for example) to be loaded from Play Store. SubscriptionsActivity#SKUS defines a list of subscription identifiers used in this example.
The UI consists of 3 adapter-like views:
- #mAvailableSkus Spinner, representing SKUs available for purchase
- #mPurchasedSkus RecyclerView which contains purchased subscriptions
- #mTargetSkus Spinner, representing SKUs available for “replacement”
All of the adapters are notified every time the inventory is reloaded in SubscriptionsActivity#reloadInventory method.
When user presses “Buy” button we start a purchase flow in the same way as for any other in-app (see previous uses cases for details). After the purchase is gone through user can select the purchased subscription and change it to another subscription selected in the spinner to the left of “Change” button. This triggers getBuyIntentToReplaceSkus from the billing API at the end of which the new subscription replaces the old one in the “purchased subscriptions” section.
That’s it for now. If you have any questions or suggestions (like use cases you want to be covered) don’t hesitate to write them in the comments.
Found a bug in the library? Feel free to submit a bug report on Github.