Reputation: 1737
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?
Upvotes: 23
Views: 6680
Reputation: 11
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:
Create a ViewModel and declare the BillingClient object, like this:
class BillingViewModel : ViewModel() {
var billingClient: BillingClient? = null
}
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
}
Upvotes: 1
Reputation: 40522
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.
Upvotes: 8
Reputation: 39563
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.
Upvotes: 8
Reputation: 915
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.
Upvotes: -1
Reputation: 144
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.
Upvotes: -2
Reputation: 12118
'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.
Upvotes: -1
Reputation: 1737
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.
Upvotes: 4