One instance of BillingClient throughout app
Asked Answered
O

7

23

I have an app that has many activities. One of the activity is for showing purchase options.

In the sample apps for the billing library (https://github.com/googlesamples/android-play-billing), BillingClientLifecycle and BillingManager are used, both of which are associated to a single activity, so the connection is opened/closed when the activity is created/destroyed.

However in an app with many activities, it seems not ideal to do this separately for different activities. I also want to check on app start whether the subscriptions are valid.

I am thinking of creating the BillingClient in the app's Application subclass. However if I do this, I would only be opening the BillingClient connection and not closing it (as there's no onDestroy method in there). Has anyone done this before and hit any issues? Also is this against best practices and will it cause any issues with network / performance?

Oratorian answered 2/11, 2018 at 2:3 Comment(0)
O
4

It looks like this can be done with architecture components. I.e. in your application's OnCreate, call:

ProcessLifecycleOwner.get().lifecycle.addObserver(billingClient)

And just inject the billingClient into the activities that need it.

Oratorian answered 2/11, 2018 at 3:32 Comment(3)
But if we do that, nothing will ever call BillingClient.endConnection, right? Is that safe? It seems like it might be safe, because the connection should safely disconnect when the app terminates, but I can't tell for sure.Repeater
I’ve been using one connection throughout my app and it seems fine, I haven’t hit issues. I think endConnection is needed if you open more than one connection because there is a potential for resource/memory leaks.Oratorian
Looks like this answer not up to date. With compose we have single activity App. And in this case billing manager will be on the background for all screens until we have app in the foreground. I guess we should connect here compose scree lifecycle instead.Shashaban
R
7

I read through the sources of BillingClientImpl.java in billing-1.2.2-sources.jar, and I believe it is safe to use BillingClient as an application singleton, even if this means never calling BillingClient.endConnection().

BillingClientImpl.java doesn't need/use an Activity in its constructor; it uses a Context, and all it does is call context.getApplicationContext() to store the app context. The launchBillingFlow method does have an Activity parameter, but the activity isn't stored; its only purpose is to call activity.startActivity(intent) with the billing intent.

BillingClient.startConnection calls context.registerReceiver to register its own BillingBroadcastReceiver as a BroadcastReceiver, then calls context.bindService to bind a service connection. (Again, both of these calls are executed against the app context mApplicationContext, not on any particular Activity.)

As long as the billing client is required for the lifetime of the app, it's safe and acceptable to call registerReceiver and bindService in Application.onCreate() and to never call unregisterReceiver or unbindService.

This would not be safe if the registerReceiver and/or bindService calls used an Activity context, because the ServiceConnection would leak when the Activity was destroyed, but when the app is destroyed, its process terminates, and all of its service connections are automatically cleaned up.

Repeater answered 1/5, 2019 at 6:3 Comment(2)
Dan - I like your suggestion to use a Singleton, but I have one question regarding PurchasesUpdatedListener. You must provide this when initialising the BillingClient, but ideally I would like to register two different listeners so that I return to the initiating activity once the purchase is complete. How would I achieve this? Currently I can only see a way of placing the listener in the singleton class, or passing it in at initialisation, but again you are limited to one listener. Thoughts?Jaenicke
My singleton implements PurchasesUpdatedListener, and my other IAP-aware Activity classes also implement PurchasesUpdatedListener. In my singleton's onPurchasesUpdated implementation, I callback all of the currently registered PULs. My Activity classes unregister themselves with the singleton during onStop.Repeater
C
7

Regarding the updated 2.x version of the billing library, a quote from the TrivialDriveKotlin official demo app BillingRepository sources:

Notice that the connection to [playStoreBillingClient] is created using the applicationContext. This means the instance is not [Activity]-specific. And since it's also not expensive, it can remain open for the life of the entire [Application]. So whether it is (re)created for each [Activity] or [Fragment] or is kept open for the life of the application is a matter of choice.

I guess this applies to the first version too.

Coquet answered 23/8, 2019 at 11:50 Comment(0)
O
4

It looks like this can be done with architecture components. I.e. in your application's OnCreate, call:

ProcessLifecycleOwner.get().lifecycle.addObserver(billingClient)

And just inject the billingClient into the activities that need it.

Oratorian answered 2/11, 2018 at 3:32 Comment(3)
But if we do that, nothing will ever call BillingClient.endConnection, right? Is that safe? It seems like it might be safe, because the connection should safely disconnect when the app terminates, but I can't tell for sure.Repeater
I’ve been using one connection throughout my app and it seems fine, I haven’t hit issues. I think endConnection is needed if you open more than one connection because there is a potential for resource/memory leaks.Oratorian
Looks like this answer not up to date. With compose we have single activity App. And in this case billing manager will be on the background for all screens until we have app in the foreground. I guess we should connect here compose scree lifecycle instead.Shashaban
N
1

I had the same problem and after 2 days of brainstorming I found an updated solution.

In my case I'm using Navigation Component, that is, I have a MainActivity as a container for the fragments.

The magic solution is quite simple actually:

  1. Create a ViewModel and declare the BillingClient object, like this:

     class BillingViewModel : ViewModel() {
    
     var billingClient: BillingClient? = null
    

    }

  2. In the MainActivity, declare the ViewModel, create an instance of BillingClient in the onCreate() method, and pass this value to the ViewModel's billingClient object, like this:

    class MainActivity : AppCompatActivity() {
         private val viewModel by viewModels<BillingViewModel>()
         private var billingClient: BillingClient? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
            billingClient = BillingClient.newBuilder(applicationContext)
                .setListener(MyPurchasesUpdatedListener(this, this))
                .enablePendingPurchases()
                .build()
            viewModel.billingClient = billingClient
        }
    }

Okay, now you can get that same instance in any fragment you need it.

But there is one last important thing for you to consider, pay attention:

When declaring the ViewModel in some fragment, do it this way:

class YourFragment : Fragment() {
    private val viewModel by activityViewModels<BillingViewModel>()
    private var billingClient: BillingClient? = null
}

It is very important that you use "activityViewModels()" to declare the ViewModel, do not confuse it with "viewModels()", there is a difference here. Using "activityViewModels()" you guarantee that you are accessing the same BillingClient instance that was initialized in the MainActivity.

Now, just pass the billingClient value from the viewModel to the billingClient object from your fragment in onCreateView, like this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        billingClient = viewModel.billingClient
    }
Nullipore answered 12/5, 2023 at 4:36 Comment(0)
P
-1

'I would suggest not to make a Singleton class that provides BillingClient from it and initialized through application class.'

Why? Because, in doing such way, you've chance to leak memory or object while using throughout app.


Alternate way is to make such class as LifecycleObserver, so that once you bind it to your Activity/Fragment though respects it's lifecycle and do stuffs accordingly.

I've created such class as below and using in some of my projects (In which, it's working pretty well).

Check out the class below :

InAppBilling.kt

class InAppBilling(val lifecycle: Lifecycle, purchasesUpdatedListener: PurchasesUpdatedListener) :
    LifecycleObserver,
    PurchasesUpdatedListener by purchasesUpdatedListener,
    BillingClientStateListener {

    companion object {
        const val TAG = "InAppBilling"
    }

    init {
        lifecycle.addObserver(this)
    }

    private var mBillingClient: BillingClient? = null

    private fun initClient(): BillingClient {
        return BillingClient
                .newBuilder(context) // You can provide application context here.
                .setListener(this)
                .build()
    }

    @OnLifecycleEvent(value = Lifecycle.Event.ON_CREATE)
    fun connectionToBillingServer() {
        mBillingClient = initClient()
        mBillingClient?.startConnection(this)
    }

    @OnLifecycleEvent(value = Lifecycle.Event.ON_DESTROY)
    fun disconnectFromBillingServer() {
        mBillingClient?.endConnection()
        lifecycle.removeObserver(this)
    }

    override fun onBillingServiceDisconnected() {
        if (lifecycle.currentState == Lifecycle.State.CREATED) {
            mBillingClient?.startConnection(this)
        }
    }

    override fun onBillingSetupFinished(responseCode: Int) {
        // Left out because "Unused"
    }
}

And how to consume it :

Inside of your Activity/Fragment where you want to use BillingClient:

private var mBillingClient: InAppBilling? = null

//.. And inside of onCreate() method, you'll just need to initialize it like below:
    mBillingClient = InAppBilling([email protected], this)

Now, you can use this billingClient object to do stuffs that you want from In-App client.

All you'll need to do is to add your specific method to InAppBilling class and consume that method where you wanted it.

Check out the same class from this gist.

Pubis answered 25/4, 2019 at 6:1 Comment(5)
I offered a bounty on this question because I have multiple Activity classes that need to access billing, including several in the same task (back stack). Your approach will create multiple BillingClients, one per Activity, right? And each BillingClient will manage its own connection? Even though the Lifecycle will clean all of them up, is this wise? It may not be a memory leak, but it seems like it would be wasteful to have multiple open connections when one would be enough.Repeater
Yeah, I saw this question after bounty and after looking at @mliu's answer, it got me encouraged to post the same thing I've done so far. Yes you're right @Dan that this will create multiple instance of billing client but Google didn't mention anywhere that 'it's resource hungry class, so avoid making multiple instance per app' & so my code is helpful due to it's child of LifecycleObserver. Even though it's your call whether use it or not, I just wanted to share my work done so far yet :)Pubis
Google does mentioned that please keep just one BillingClient connected at one time. It's strongly recommended that you have one active BillingClient connection open at one time to avoid multiple PurchasesUpdatedListener callbacks for a single event. Therefore, if each activity has its own BillingClient, it must stop the connection on activity paused instead of on destroyed. However, will that becomes a problem when BillingClient show the purchase dialog (which shall also pause the activity).Abele
Yeah, on that contrary I'd suggest to bind this class via process lifecycle owner so that it's shared across the activities and through entire app.Pubis
My only point here was to utilise lifecycle components since it's available for Android to avoid unexpected behaviour or memory leaks while it being singleton.Pubis
S
-1

Make a BaseActivity and let all your other activities extend base activity. Create instance of billing in BaseActivity.

No need to do it application class. As in Application you don't get an event when app is exited. In addition, If you put app in background, application instance is still present, hence connection will be kept open unnecessarily.

Spicy answered 30/4, 2019 at 4:21 Comment(4)
This approach will create multiple BillingClients, one per Activity, right? And each BillingClient will manage its own connection? Even though the BaseActivity will clean all of them up, is this wise? It may not be a memory leak, but it seems like it would be wasteful to have multiple open connections when one would be enough.Repeater
If multiple billing clients is a concern, you can make it a singleton. But if I were you, I will not worry about each activity instantiating its own client because as Dalvik does a pretty great job in clearing objects relying on activity lifecycle.Denton
I'm not much worried about wasting memory on the objects; I'm worried about maintaining open connections. Connections are a scarce resource.Repeater
Google's documentation says It's strongly recommended that you have one active BillingClient connection open at one time to avoid multiple PurchasesUpdatedListener callbacks for a single event. When navigating to another activity, previous activity is not destroyed but paused. Then both activities will receive callbacks for a single event.Abele
R
-2

BillingClient requires current activity because it needs current window token to show purchase dialog to user. So every time activity changes window token also changes so you can not do this with a singleton class because in singleton class you are leaking activity reference and also providing a single window token which is not valid through your app session.

Rowles answered 30/4, 2019 at 2:24 Comment(1)
I don't think this is true. (Correct me if I'm wrong and I'll reverse my downvote.) I read through the sources in billing-1.2.2-sources.jar and I see no reference to any window token. BillingClientImpl.java doesn't need/use an Activity in its constructor; it uses a Context, and all it does is call context.getApplicationContext() and store the app context. The launchBillingFlow method does have an Activity parameter, but the activity isn't stored; its only purpose is to call activity.startActivity(intent) on the billing intent.Repeater

© 2022 - 2024 — McMap. All rights reserved.