0

I have a situation where my BillingDataSource aka BillingClientWrapper needs a singleton BillingClient object, but creating the BillingClient object can't be completed without a listener that's in the BillingDataSource:

class BillingDataSource(private val client: BillingClient) {
    private val _purchases = MutableStateFlow<List<Purchase>?>(mutableListOf())
    val purchases = _purchases.asStateFlow()

    val PURCHASES_UPDATED_LISTENER = PurchasesUpdatedListener { result, purchases ->
        _purchases.value = purchases
    }
}

And the dependency injection:

@Provides
@Singleton
fun provideBillingClient(
    @ApplicationContext context: Context,
    wrapper: BillingDataSource
) : BillingClient {
    return BillingClient.newBuilder(context)
        .setListener(wrapper.PURCHASES_UPDATED_LISTENER)
        .enablePendingPurchases()
        .build()
}

@Provides
@Singleton
fun provideBillingDataSource(
    client: BillingClient
) : BillingDataSource = BillingDataSource(client)

The .setListener requirement when constructing the BillingClient is turning out to be a real headache. Of course, I can break the circular dependency by putting the listener outside of the BillingDataSource, but then I'd lose access to members (like _purchases) inside of BillingDataSource, and that's hardly ideal.

How do I solve this?

4
  • The best way is to register the listener after both objects have been created. Give BillingClient some kind of init method that creates and registers the listener. Commented Sep 20, 2023 at 15:00
  • @Jorn, I mentioned both objects need each other to be created successfully. In any case, I'd be unable to register the listener after the .build() call. Commented Sep 20, 2023 at 15:13
  • Right, and my advice is to break that dependency so that you can do your initialization properly without needing a circular dependency. If you currently need that listener in the constructor, you are probably doing too much work in that constructor anyway. Commented Sep 20, 2023 at 15:15
  • Oh, I forgot to mention the BillingClient is not a class under my control (it's part of the Billing Library provided by Google). So it's only the BillingDataSource I'm able to modify. Commented Sep 20, 2023 at 15:18

2 Answers 2

2

You can extract purchase listener (and possibly some other related methods and properties) to a separate class or object.

// instead of dependency injection 
// you can use object here for PurchaseListener
@Singleton 
class PurchaseListener @Inject constructor(){
    private val _purchases = MutableStateFlow<List<Purchase>?>(mutableListOf())
    val purchases = _purchases.asStateFlow()

    val callback = PurchasesUpdatedListener { result, purchases ->
        _purchases.value = purchases
    }
}
@Singleton
class BillingDataSource @Inject constructor(
             private val client: BillingClient, 
             val purchaseListener: PurchaseListener)

@Provides
@Singleton
fun provideBillingClient(
    @ApplicationContext context: Context,
    purchaseListener: PurchaseListener
) : BillingClient {
    return BillingClient.newBuilder(context)
        .setListener(purchaseListener)
        .enablePendingPurchases()
        .build()
}

@Provides
@Singleton
fun provideBillingDataSource(
    client: BillingClient,
    purchaseListener: PurchaseListener
) : BillingDataSource = BillingDataSource(client, purchaseListener)
Sign up to request clarification or add additional context in comments.

1 Comment

And what about a situation where the PurchaseListener also needs a BillingClient? Same dependency cycle problem. Tricky!
1

Since you are creating a wrapper anyway, it makes sense to have the BillingClient as an internal dependency to the BillingDataSource. Then the BillingDataSource becomes the only way for the rest of the application to interact with it:

class BillingDataSource(@ApplicationContext context: Context) {
    private val _purchases = MutableStateFlow<List<Purchase>?>(mutableListOf())
    val purchases = _purchases.asStateFlow()

    private val client = BillingClient.newBuilder(context)
        .setListener { result, purchases ->
            _purchases.value = purchases
        }
        .enablePendingPurchases()
        .build()

    // expose other interactions with client here
}

Generally it's not a good idea to create dependencies inside your constructor, but in this case it doesn't break testability, since all mocking or stubbing can be done on the BillingDataSource level.

2 Comments

Yes, this is actually how it's done in the official docs. But they're not using Hilt in the docs so I was hoping to get a cleaner approach here. If nothing else, I'll end up settling for this.
This is only less clean if you plan on using the BillingService elsewhere. Which, if I understand correctly, you don't.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.