Dagger2.10+: Inject ViewModel in Fragment/Activity which has run-time dependencies
Asked Answered
B

1

5

For ViewModels which has only compile-time dependencies, I use the ViewModelProvider.Factory from Architecture components like following:

class ViewModelFactory<T : ViewModel> @Inject constructor(private val viewModel: Lazy<T>) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModel.get() as T
}

And in my Activity or Fragment I get the ViewModel in following way:

@Inject
lateinit var viewModelFactory: ViewModelFactory<ProductsViewModel>

This is working fine until my ViewModel needs a dependency which is only available at run-time.

Scenario is, I have a list of Product which I am displaying in RecyclerView. For each Product, I have ProductViewModel.

Now, the ProductViewModel needs variety of dependencies like ResourceProvider, AlertManageretc which are available compile-time and I can either Inject them using constructor or I can Provide them using Module. But, along with above dependencies, it needs Product object as well which is only available at run-time as I fetch the list of products via API call.

I don't know how to inject a dependency which is only available at run-time. So I am doing following at the moment:

ProductsFragment.kt

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        productsAdapter = ProductsAdapter(context!!, products, R.layout.list_item_products, BR.productVm)
        rvProducts.layoutManager = LinearLayoutManager(context)
        rvProducts.addItemDecoration(RecyclerViewMargin(context, 10, 20))
        rvProducts.adapter = productsAdapter
        getProducts()
    }

private fun getProducts() {
    productsViewModel.getProducts()
            .observe(this, Observer { productResponse: GetProductResponse ->
                products.clear()
                productsAdapter?.notifyDataSetChanged()
                val productsViewModels = productResponse.data.map { product ->
                   // Here product is fetched run-time and alertManager etc are
                   // injected into Fragment as they are available compile-time. I
                   // don't think this is correct approach and I want to get the
                   // ProductViewModel using Dagger only.
                    ProductViewModel(product, resourceProvider,
                            appUtils, alertManager)
                }
                products.addAll(productsViewModels)
                productsAdapter?.notifyDataSetChanged()
            })
}

ProductsAdapter binds the ProductViewModel with the list_item_products layout.

As I mentioned in comments in the code, I don't want to create ProductViewModel my self and instead I want it from dagger only. I also believe the correct approach would be to Inject the ProductsAdapter directly into the Fragment, but then also, I need to tell dagger from where it can get Product object for ProductViewModel which is available at run time and it ends up on same question for me.

Any guide or directions to achieve this would be really great.

Brown answered 2/9, 2019 at 6:28 Comment(3)
Have you tried AssistedInject ? I think this is what you are looking for. I also have this article in case you want to know how to use it.Oglethorpe
duplicate of 1 2Satisfy
@coroutineDispatcher Thanks for the link, I'll check it quickly :)Brown
A
6

You are on the right direction in wanting to inject dependencies instead of creating them like you are doing with ProductViewModel. But, yes, you can't inject ProductViewModel as it needs a Product which is only available a runtime.

The solution to this problem is to create a Factory of ProductViewModel:

class ProductViewModel(
    val product: Product, 
    val resourceProvider: ResourceProvider,
    val appUtils: AppUtils, 
    val alertManager: AlertManager
) {
// ...
}

class ProductViewModelFactory @Inject constructor(
    val resourceProvider: ResourceProvider,
    val appUtils: AppUtils, 
    val alertManager: AlertManager
) {
    fun create(product: Product): ProductViewModel {
        return ProductViewModel(product, resourceProvider, appUtils, alertManager)
    }  
}

Then inject ProductViewModelFactory in your ProductsFragment class, and call productViewModelFactory.create(product) when the Product is available.


As your project start getting bigger and you see this pattern repeating, consider using AssistedInject to reduce the boilerplate.

Absquatulate answered 4/9, 2019 at 9:52 Comment(1)
Looks like a way to go. Thanks, I'll check this by implementing shortly :)Brown

© 2022 - 2024 — McMap. All rights reserved.