Table of contents
My app on the Google Playstore
GitHub code
Introduction
- This series will be an informal demonstration of a problem I faced and how I solved it. Each blog post in this series will be unique and separate from the others, so feel free to look around.
Problem with ViewModels and Google's Billing library
- Reading through the Google Play billing library, I ran across this section:
It's strongly recommended that you have one active BillingClient connection open at one time to avoid multiple PurchasesUpdatedListener callbacks for a single event.
Reading this out loud it seems logical and nothing to worry about. However, it lead me to ask the question,
When is my code creating a BillingClient and how is it even possible create multiple of them?
Well as it turns out, I have the BillingClient HERE and creating instances of it is delegated to a ViewModel HERE. So that means that every time I create a certain ViewModel, a BillingClient will get created and a PurchasesUpdatedListener callback will be created. This then leads us to the question of,
When does our ViewModel get created?
. To answer that question we have to learn a little about ViewModel Scopes.
ViewModel Scopes
- Whenever a ViewModel is created it is scoped to an object that implements the
ViewModelStoreOwner
interface. This can be an Activity, Fragment, Navigation graph or any other class that implements the ViewModelStoreOwner interface. This is important to think about because the lifecycle of a VIewModel is tied directly to its scope. Meaning a ViewModel remains in memory until the ViewModelStoreOwner to which it is scoped disappears. This may occur for the following reasons:
1) When a Activity finishes(dismissed by the user)
2) When a Fragment detaches from the FragmentManager
3) During navigation, when its removed from the back stack
Scoping APIs
- In order to scope our ViewModels we will be focusing on these two extension functions:
1) viewModels() : This extension function will scope the ViewModel to the closest VIewModelsStoreOwner. If this is used inside a fragment(Which is what I use) it will be scoped to the fragment. If used inside of a Activity it will be scoped to the Activity.
2) activityViewModels() : by using this extension function we are able to get an activity scoped ViewModel from inside of a Fragment. So inside for a Fragment's onCreateView() method we can do this:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val billingViewModel: BillingViewModel by activityViewModels()
- This will allow our
billingViewModel
to last as long as the Activity does. This gets very interesting when you use theval billingViewModel: BillingViewModel by activityViewModels()
in multiple fragments. What do you think happens? Does another Activity scoped ViewModel get created or does the Android system resuse the same instance?????? Correct! The Android system resuse the same instance.
The multiple BillingClient connection problem
- So now that we both know a little bit about scoping we can talk about the
multiple BillingClient connection problem
and why a combination of scoping to Fragments and the Navigation Component causes it. In my app I use the Navigation Component which handles all the fragment instantiation itself, which is really awesome. Themultiple BillingClient connection problem
arises when we scope our ViewModel to a fragment and then our user navigates around our app, without popping fragments from the back stack. At a high level the problematic navigation looks something like this:
Where the arrows demonstrate each time the user navigates from the Home Fragment to the Subscription Fragment.
Now since the ViewModel is scoped to the the Subscription Fragment. Each time the user navigates to that Fragment the Navigation component will create a new instance of the Subscription Fragment, which creates a new instance of the BillingViewModel, which creates a new BillingClient instance, creating a PurchasesUpdatedListener callback and each one of these instances are being stored on the back stack. This results in there being multiple
PurchasesUpdatedListener
callbacks(the real trouble makes) for a single event.
Fixing the multiple BillingClient connection problem
- The solution to this problem is very easy and we only really need to change the scope of the ViewModel from the Fragment to the Activity with the
activityViewModels()
extension function. So inside of our Fragment'sonCreateView()
we can do this:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val billingViewModel: BillingViewModel by activityViewModels()
- Now the
billingViewModel
can be passed to our composables:
binding.composeView.apply{
setContent {
// composable function
MainView(
billingViewModel = billingViewModel
)
}
- As I mentioned earlier the
activityViewModels()
is very interesting because it will only create one instance of thebillingViewModel
and then if we callval billingViewModel: BillingViewModel by activityViewModels()
from any other Fragment the system will reuse that same instance. So as Fragments get created and destroyed automatically via the Navigation Component thebillingViewModel
will only be destroyed once the Activity is entirely dismissed by the user and is destroyed.
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)