google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.45k stars 2.02k forks source link

Inject Dependencies in AndroidViewModel with default factory and constructor #1273

Closed pyus13 closed 4 years ago

pyus13 commented 6 years ago

It looks like AndroidInjection only allows to inject dependencies in Android main components and Fragment. With the new MVVM pattern my most of the application logic lies in AndroidViewModel classes and I want to inject dependencies there.

One way is to write a custom factory and pass the dependencies for every single ViewModel in the app.

It is too much work and a lot of boilerplate code. Also, it forces me to update the factory whenever I need to pass new dependencies in my ViewModel. Then I need to update my tests for ViewModels.

Cant dagger ship anything which makes dependencies injection in AndroidViewModel classes possible. AndroidViewModels are not supposed to be created by calling constructors so DI if the perfect solution for it.

ashdavies commented 6 years ago

Currently you can use Multibinding to create a generic view model factory, it's less than ideal, but it means you don't have to create or update your view model factory.

https://gist.github.com/ashdavies/2a96facbbe766f404d1b770182255da9

mt-mitchell commented 6 years ago

A good example I found was the googlesamples/android-architecture-components GitHub example ViewModelFactory. And the Injector object they use makes it easy to remove a lot of the boilerplate.

The example does force you to use Unscope/Singleton dependencies for ViewModels but with the ViewModel cache this is a little moot since the factory will not get called, and the context should hit the ViewModel store cache. You can scope the factory, but, to get it all started this works well enough.

Ufkoku commented 6 years ago

As workaround I developed a lib, which generates Factory classes for ViewModels. Usage example:

  1. ViewModel
  2. Dagger module

But it doesn't help with tests.

matejdro commented 5 years ago

So far I've found this to be the best solution:

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

With that, you just add your ViewModel to Dagger however you want (with @Inject constructor, with module etc.) and then Inject InjectableViewModelFactory<YourViewModelType> into your Activities/Fragments. It retains scope and everything.

For tests, you just instantiate your mocked ViewModel manually and create instance of this class manually: viewModelFactory = InjectableViewModelFactory(Lazy { viewModel })

ashdavies commented 5 years ago

@matejdro I've started using a similar approach, this is also similar on the Plaid app, though I think it better to use Provider<VM> instead of Lazy<VM>.

matejdro commented 5 years ago

From what I see, Plaid app has separate factory class for every single ViewModel? So it is not the same approach.

I'm not sure if Provider vs Lazy makes a difference, factory is not supposed to be called multiple times, right?

ashdavies commented 5 years ago

Similar, not the same. Yes, the activity ViewModelStore will manage the lifecycle of the ViewModel, thus will only call get() when it is necessary, so Provider is only required to know how to generate the instance, whereas Lazy will store a reference to be re-used, and often uses a synchronised thread-safe algorithm.

ronshapiro commented 5 years ago

I think the solution that we come up with for #1183 should be applicable here (i.e. allow @ContributesAndroidInjector to work for view models)?

matejdro commented 5 years ago

Personally I do not think @ContributesAndroidInjector is a good idea, since it forces you to use field injection. ViewModel is not initialized by framework without a way to intercept like activities, so you can just use constructor injection.

ashdavies commented 5 years ago

Agreed that field injection shouldn't be used when constructor injection is available, it seems that the @AssistedInject presented by @JakeWharton at last years Droidcon UK is the preferred option by many

davida5 commented 4 years ago

So far I've found this to be the best solution:

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

With that, you just add your ViewModel to Dagger however you want (with @Inject constructor, with module etc.) and then Inject InjectableViewModelFactory<YourViewModelType> into your Activities/Fragments. It retains scope and everything.

For tests, you just instantiate your mocked ViewModel manually and create instance of this class manually: viewModelFactory = InjectableViewModelFactory(Lazy { viewModel })

I'm not able to use your suggestion. Is it still valid with the latest dagger? I keep getting an error about "Not able to provide ViewModel without @Provides annotation" even though it has an @Inject constructor and I have @ContributesAndroidInjector on the fragment... Do you have any other examples other than the WearMusicCenter application? It seems a bit outdated

matejdro commented 4 years ago

It still works normally in latest dagger. You likely have some other error. I would move that discussion to stack overflow though.

davida5 commented 4 years ago

@matejdro Thanks for the reply. I created a stack overflow post, have a look if you have time. https://stackoverflow.com/questions/59381817/injecting-viewmodel-into-the-fragment-with-dagger-2-with-parameters

tir38 commented 4 years ago

I'm implementing something similar and I want to make sure I understand the OP's problem (and that I don't have the same problem w/out yet knowing it ;) )....

The OPs issue comes from wanting a single Factory for all of their ViewModels. If instead they created a separate Factory for each VM then they could do what Plaid does: example: use constructor injection on factory and then pass dependencies right into VM constructor, they they would not have this problem, right?

Side note @pyus-13 where did you read "AndroidViewModels are not supposed to be created by calling constructors" ? I can't seem to find that anywhere. I thought that is exactly what the Factory was for... calling VM constructors.

baughmann commented 4 years ago

Google, where is the official solution? This is an architectural problem between two fully-supported Google libraries that has been known for at least two years.

Am I missing something? Is this on the roadmap?

Chang-Eric commented 4 years ago

Similar to https://github.com/google/dagger/issues/1271, since Hilt has been released along with the Jetpack extension for ViewModels, that is going to be the official ViewModel injection solution. dagger.android is unlikely to get an update to support ViewModels, so I'm going to close this.

tir38 commented 4 years ago

This is what @Chang-Eric is referring to: https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodels

Sadly I wish there was a "vanilla" Dagger (aka w/out dagger-android or Hilt) way of doing this. I really don't want to use Hilt just for this one thing.