CodeNewbie Community 🌱

Cover image for ViewModel scoping and the Google Play Billing library
Tristan
Tristan

Posted on

ViewModel scoping and the Google Play Billing library

Table of contents

  1. Introduction
  2. Problem I am solving
  3. ViewModel Scopes
  4. Recreating the problem
  5. Fixing the problem

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

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()

Enter fullscreen mode Exit fullscreen mode
  • This will allow our billingViewModel to last as long as the Activity does. This gets very interesting when you use the val 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. The multiple 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:

problematic navigation

  • 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's onCreateView() we can do this:
override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val billingViewModel: BillingViewModel by activityViewModels()

Enter fullscreen mode Exit fullscreen mode
  • Now the billingViewModel can be passed to our composables:
binding.composeView.apply{
            setContent {

                // composable function
                MainView(
                    billingViewModel = billingViewModel
                )
            }
Enter fullscreen mode Exit fullscreen mode
  • As I mentioned earlier the activityViewModels() is very interesting because it will only create one instance of the billingViewModel and then if we call val 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 the billingViewModel 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)