RxBilling: Android billing library
Hello everybody! My name is Dmytro Ostapovets. I am an Android engineer of the BetterMe project at the Genesis company.
Subscriptions are the main way to monetize our projects. It all looks fairly simple if your app has one or two screens offering subscriptions, as well as a couple of access points to a paid content.
However, this simplicity vanishes away as soon as you have to tie subscription status check almost to every user’s click, send analytics concerning successful/canceled subscription tries and launch a couple of A/B tests.
In this article, I’m going to share our work experience with instruments like Google Play Billing Library and IInappBillingService. Also, I’ll tell you a couple of words about our little billing library.
For a fairly long time, we used to exploit IInappBillingService wrapped up in a simple Helper — class (pretty much like everybody). The implementation was, to put it mildly, not great.
Google launched Play Billing Library — a library for dealing with purchases. It was promising as it was supposed to solve nearly all problems that emerged when dealing with IInappBillingService.
After trying out the library, studying the code and issues on GitHub, we weren’t satisfied by a couple of points:
- Connection: you must check the BillingClient’s state before every operation. Unless it’s connected, you have to wait and put this operation in a queue. Guys from Google recommend to do it in the following way:
- Overly generalized listener —
onPurchasesUpdated, that catches updates from all operations. If you don’t need too strict analytics like which product purchase was canceled by the user, this callback will, probably, suit you. - Mentioned onPurchasesUpdated can be called twice in a response to only one operation. If your analysts try to analyze events like success and cancel, quite possible they will be unpleasantly surprised. Link to the bug: github.com/…id-play-billing/issues/83
Usually typical logic of subscription status checks (whether it’s with BillingClient or the IInappBillingService) looks approximately like the following:
override fun onStart() {
super.onStart()
billingManager.connectBilling(this)
}override fun onStop() {
billingManager.disconnectBilling()
super.onStop()
}override fun onConnected() {
billingManager.getPurchases()
}
It’s quite inconvenient if you consider 10 screens running checks like these. Operations like getHistory() and getDetails() with theirs callbacks will be added to the picture, and all that has to be merged somehow. Besides, hiding check logic is problematic in, let’s say, UseCase, while still facing the need to tie connect/disconnect to the UI lifecycle.
So, we figured to write a tiny wrapper over BillingClient and IInappBillingService.
Here I listed main requirements:
- connect / disconnect according to the LifecycleOwner lifecycle;
- checking BillingClient status before each operation(get connected, wait and perform the operation);
- sharing already connected BillingClient with all subscribers;
- repeat / retry logic;
- strict delimitation of the purchase’s events: which kind of product, the result of the operation
RxBilling
We used RxJava for connection control and the library overall (it’s not trendy to write about Rx, but here we are anyway).
For the starters, we write a small interface, which basically represents all or most of the BillingClient functions, wrapped up in Observable:
Now a couple of words about fun connect(): Flowable<BillingClient>.
The idea was to let the client call connect(), then subscribe to Flowable and maintain the BillingClient connection until disposable.dispose() is called.
To connect/disconnect BillingClient according to Activity / Fragment lifecycle, we create BillingConnectionManager. It implements LifecycleObserver and calls subscribe/dispose in onStart / onStop :
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun connect() {
disposable = connectable.connect()
.subscribe()
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun disconnect() {
disposable?.dispose()
}Next step is to register our BillingConnectionManager in LifecycleOwner.
lifecycle.addObserver(billingConnectionManager)
Before performing any kind of operation we should make sure that the BillingClient state is the one we need. If it’s not connected — then obviously we should connect and only after that perform the intended operation. That’s how it all looks implemented:
Important note: all BillingClient methods have to be called from the UI thread. And since BillingClient can be used in various ways, for example in Repository(its functions are often executed in the background thread), it’s better to be completely sure about the rightness of the thread:
To create BillingClient we implement BillingClientFactory. Its main tasks are listed below:
- create Flowable;
- cache BillingClient and pass the same object to all following clients;
- implement repeat / retry logic;
- terminate the connection after the last client unsubscribed;
Creating Flowable looks like the following:
Upon calling function startConnection() we have to implement two callbacks BillingClientStateListener: onBillingSetupFinished() and onBillingServiceDisconnected()
If the connection was successful, we have to pass BillingClient to our subscribers, if not — notify them of errors. One of the eventualities is that there are no active subscribers when connection succeeded. In this case, connection has to be terminated — billingClient.endConnection(). Implementation of onBillingSetupFinished():
Clients have to be notified of the termination of a connection in onBillingServiceDisconnected(). Herewith client can implement repeat logic. Implementation of onBillingServiceDisconnected():
override fun onBillingServiceDisconnected() {
if (!it.isCancelled) {
it.onComplete()
}
}We use FlowableEmitter.setCancellable to implement termination of a connection after last client unsubscribed.
it.setCancellable {
if (billingClient.isReady) {
billingClient.endConnection()
}
}As I have already mentioned, it would be nice to implement caching and sharing of the same BillingClient object for all subscribers (for example, a couple of fragments in the Activity or even several Activities).
It would make possible to instantly connect to a client’s billing. This logic is implemented with the help of FlowableTransformer, default implementation — RepeatConnectionTransformer
A bit of clarification concerning operators:
share() — we use this operator to avoid creation of multiple billing clients when more than one subscriber tries to connect (almost)simultaneously. In this case, all clients are going to be subscribed to exactly one Flowable.
repeat() — allows to repeat connection after onBillingServiceDisconnected.
replay() — caches all events(in our case it’s just one event — BillingClient) and passes them to all following subscribers.
Any FlowableTransformer is passed to the constructor(by default it is RepeatConnectionTransformer) so one can implement own repeat/retry logic.
RxBillingFlow
As I’ve already said, we have to provide specific events as a success, canceled, failed, which would relate to a certain product. But as far as I know, BillingClient does not support functions like these (or maybe I didn’t find them). Instead, BillingClient emits general events that have no ties to specific actions of a user or to a certain product thereby. Moreover, it has a bug with duplication of events.
So to make purchases we’ve decided to use not the BillingClient, but the “raw” IInappBillingService instead, and if talking more specifically — we used its wrapper RxBillingFlow. IInappBillingService, unlike BillingClient, lets you launch purchases UI manually, as well as handle results by yourself(BillingClient does all the work for us).
Similarly to the BillingClientFactory, we implemented BillingServiceFactory — an object, which creates a connection to IInappBillingService. Logic is pretty identical, except service specific details:
As for the BillingServiceFactory, we use the same RepeatConnectionTransformer, to tie RxBillingFlow to the Activity / Fragment lifecycle — BillingConnectionManager.
RxBillingFlow functions :
fun buyItem(request: BuyItemRequest, delegate: FlowDelegate): Completable — purchase of the product or subscription. Returns Completable — the operation is either successful or not;
fun replaceItem(request: ReplaceItemRequest, delegate: FlowDelegate): Completable — update of the existing(purchased) subscription;
fun handleActivityResult(activityResultCode: Int, data: Intent?): Single<Purchase> — processing operation’s resut, if purchase is successful, returns Purchase, otherwise — one of the BillingException.
FlowDelegate
RxBillingFlow delegates the launch of purchase UI to the FlowDelegate to make the launch from Fragment and other components possible. We implemented a couple of standard delegates — ActivityFlowDelegate, FragmentFlowDelegate, ConductorFlowDelegate. Any other components can be implemented if needed. The code is pretty simple:
BillingException
BillingException is returned in response to unsuccessful operations with RxBilling and RxBillingFlow.
BillingException is a sealed class with various possible billing service exceptions :
Conclusions
Through this approach, we solved various puzzles: constant connection status checks, Activity — Presenter “ping-pong”. Moreover, it allows us to “hide” all the details of the work with a subscription (except the purchase itself) on the Repository and Domain level, and to cover that with tests.
If you have experience working with billing or a useful advice concerning this topic, don’t hesitate to share — I’ll gladly read that.
Library’s code, as well as usage examples, is on Github
