Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 996
Sandip Fichadiya Avatar asked Sep 02 '19 06:09

Sandip Fichadiya


People also ask

How do you inject ViewModel in activity?

The koin-android Gradle module introduces a new viewModel DSL keyword that comes in complement of single and factory , to help declare a ViewModel component and bind it to an Android Component lifecycle. Your declared component must at least extends the android.

How do I use the same ViewModel for multiple fragments?

It is technically possible to have one ViewModel for all Fragment s. However, since this one ViewModel would have to manage a number of very different use cases, it would be something like a god object. With 20 Fragments, it would have very many lines of code ...


1 Answers

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.

like image 185
Gustavo Avatar answered Sep 23 '22 15:09

Gustavo