Search code examples
androidkotlinin-app-billingdagger-hilt

BillingClient and BillingClientWrapper - circular dependency injection


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?


Solution

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