6

Dagger version is 2.25.2.

I have two Android project modules: core module & app module.

In core module, I defined for dagger CoreComponent ,

In app module I have AppComponent for dagger.

CoreComponet in core project module:

@Component(modules = [MyModule::class])
@CoreScope
interface CoreComponent {
   fun getMyRepository(): MyRepository
}

In core project module, I have a repository class, it doesn't belong to any dagger module but I use @Inject annotation next to its constructor:

class MyRepository @Inject constructor() {
   ...
}

My app component:

@Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
@featureScope
interface AppComponent {
    fun inject(activity: MainActivity)
}

In MainActivity:

class MainActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val coreComponent = DaggerCoreComponent.builder().build()

        DaggerAppComponent
                  .builder()
                  .coreComponent(coreComponent)
                  .build()
                  .inject(this)
     }

}

My project is MVVM architecture, In general:

  • MainActivity hosts MyFragment

  • MyFragment has a reference to MyViewModel

  • MyViewModel has dependency MyRepository (as mentioned above MyRepository is in core module)

Here is MyViewModel :

class MyViewModel : ViewModel() {
    // Runtime error: lateinit property repository has not been initialize
    @Inject
    lateinit var repository: MyRepository

    val data = repository.getData()

}

MyViewModel is initialized in MyFragment:

class MyFragment : Fragment() {
   lateinit var viewModel: MyViewModel

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
        ...
    }
}

When I run my app, it crashes with runtime error:

kotlin.UninitializedPropertyAccessException: lateinit property repository has not been initialize

The error tells me dagger dependency injection does't work with my setup. So, what do I miss? How to get rid of this error?

==== update =====

I tried :

class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
        val data = repository.getData()
    }

Now when I run the app, I get new error:

Caused by: java.lang.InstantiationException: class foo.bar.MyViewModel has no zero argument constructor

====== update 2 =====

Now, I created MyViewModelFactory:

class MyViewModelFactory @Inject constructor(private val creators: Map<Class<out ViewModel>,
                                            @JvmSuppressWildcards Provider<ViewModel>>): ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = creators[modelClass] ?: creators.entries.firstOrNull {
            modelClass.isAssignableFrom(it.key)
        }?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }

    }
}

I updated MyFragment to be :

class MyFragment : Fragment() {
   lateinit var viewModel: MyViewModel
   @Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

   override fun onAttach(context: Context) {
    // inject app component in MyFragment
    super.onAttach(context)
    (context.applicationContext as MyApplication).appComponent.inject(this)
}

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // I pass `viewModelFactory` instance here, new error here at runtime, complaining viewModelFactory has not been initialized
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
        ...
    }
}

Now I run my app, I get new error:

kotlin.UninitializedPropertyAccessException: lateinit property viewModelFactory has not been initialized

What's still missing?

Leem
  • 17,220
  • 36
  • 109
  • 159
  • First of all, you are trying to use `repository` during the initialization of the view model. By that time there is no way the property has been already injected. You have to either inject it in the constructor or init `data` lazily, e.g. using the `lazy` delegate. Secondly, how do you create a `MyViewModel` instance? – jsamol Dec 29 '19 at 22:54
  • @jsamol , I updated my question with `MyFragment` which shows how I created `MyViewModel` instance. Could you please give a code example for your answer? – Leem Dec 30 '19 at 06:35
  • Did you try to add the annotation `@Inject` when you declare your viewmodel in the fragment? – Liem Vo Dec 30 '19 at 09:10

2 Answers2

3

In order to inject dependencies Dagger must be either:

  • responsible for creating the object, or
  • ask to perform an injection, just like in the activities or fragments, which are instantiated by the system:
DaggerAppComponent
    .builder()
    .coreComponent(coreComponent)
    .build()
    .inject(this)

In your first approach none of the above is true, a new MyViewModel instance is created outside Dagger's control:

viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)

therefore the dependency doesn't even get initialized. Additionally, even if you'd perform the injection more manually, like in the activity, the code still would fail, because you are trying to reference the repository property during the initialization process of the object val data = repository.getData(), before the lateinit var gets a chance to be set. In such cases the lazy delegate comes handy:

class MyViewModel : ViewModel() {
    @Inject
    lateinit var repository: MyRepository

    val data by lazy { repository.getData() }

    ...
}

However, the field injection isn't the most desirable way to perform a DI, especially when the injectable objects needs to know about it. You can inject your dependencies into ViewModels using the construction injection, but it requires some additional setup.

The problem lies in the way view models are created and managed by the Android SDK. They are created using a ViewModelProvider.Factory and the default one requires the view model to have non-argument constructor. So what you need to do to perform the constructor injection is mainly to provide your custom ViewModelProvider.Factory:

// injects the view model's `Provider` which is provided by Dagger, so the dependencies in the view model can be set
class MyViewModelFactory<VM : ViewModel> @Inject constructor(
    private val viewModelProvider: @JvmSuppressWildcards Provider<VM> 
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
     override fun <T : ViewModel?> create(modelClass: Class<T>): T = 
         viewModelProvider.get() as T
}

(There are 2 approaches to implementing a custom ViewModelProvider.Factory, the first one uses a singleton factory which gets a map of all the view models' Providers, the latter (the one above) creates a single factory for each view model. I prefer the second one as it doesn't require additional boilerplate and binding every view model in Dagger's modules.)

Use the constructor injection in your view model:

class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
    val data = repository.getData()
}

And then inject the factory into your activities or fragments and use it to create the view model:

@Component(modules = [AppModule::class], dependencies = [CoreComponent::class])
@featureScope
interface AppComponent {
    fun inject(activity: MainActivity)
    fun inject(fragment: MyFragment)
}

class MyFragment : Fragment() {

   @Inject
   lateinit var viewModelFactory: MyViewModelFactory<MyViewModel>

   lateinit var viewModel: MyViewModel

   override fun onAttach(context: Context) {
      // you should create a `DaggerAppComponent` instance once, e.g. in a custom `Application` class and use it throughout all activities and fragments
      (context.applicationContext as MyApp).appComponent.inject(this)
      super.onAttach(context)
   }

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProviders.of(this, viewModelFactory)[MyViewModel::class.java]
        ...
    }
}
jsamol
  • 3,042
  • 2
  • 16
  • 27
  • You said `viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)` a new MyViewModel instance is created outside Dagger's control. But `ViewModelProviders` is a `androidx` package thing, why it has to be bound with dagger in the 1st place? (imaging if I use another DI framework) or I don't use DI framework just use old tedious non DI way, does it mean then I couldn't use `ViewModelProviders` from `androidx` package then? I don't understand this comment from you, could you please clarify more? – Leem Dec 30 '19 at 14:51
  • and...now I get error `lateinit property viewModelFactory has not been initialized ` – Leem Dec 30 '19 at 15:11
  • @Leem *a new MyViewModel instance is created outside Dagger's control* - I meant by that that it isn't created with any use of Dagger, that's why you need to provide this custom `ViewModelProvider.Factory`, to let Dagger interfere in the process. – jsamol Dec 30 '19 at 18:44
  • @Leem *now I get error `lateinit property viewModelFactory has not been initialized`* - you have to tell Dagger to inject into the fragment, just like you did with your activity. I left the appropriate code in my answer, but I assumed you moved the process of initializing the `AppComponent` to a custom `Application`, so you can use the same instance in every activity and fragment. – jsamol Dec 30 '19 at 18:46
  • if I have moved the process of initializing the `AppComponent` to a custom `Application` e.g. `MyApplication`, do I still need to add `fun inject(fragment: Fragment)` inside `AppComponent` ? and if you check @BMacedo's answer, he mentioned a thing `ViewModelKey` which is not in your answer, is it necessary? – Leem Dec 30 '19 at 20:33
  • @Leem Yes, you have to keep all the `fun inject(...)` methods for every activity and fragment you'd like to inject; *and if you check @BMacedo's answer, he mentioned a thing ViewModelKey which is not in your answer, is it necessary?* - I've mentioned there are 2 approaches of implementing `ViewModelProvider.Factory`, @BMacedo used the first one in his answer, I went with the second. In such case `ViewModelKey` is not necessary. – jsamol Dec 30 '19 at 20:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/205104/discussion-between-leem-and-jsamol). – Leem Dec 30 '19 at 20:45
2

A few steps you'll need to use Dagger with the AAC ViewModel classes:

  1. You need to use constructor injection in your ViewModel class (as you're doing in the updated question)
  2. You will need a ViewModelFactory to tell the ViewModelProvider how to instantiate your ViewModel
  3. Finally, you will need to tell Dagger how to create your ViewModelFactory

For the first step, pass the repository in the ViewModel constructor and annotate your view model class with @Inject:

class MyViewModel @Inject constructor(private val repository: MyRepository): ViewModel() {
    val data = repository.getData()
}

For the second and third steps, one easy way to create a generic ViewModelFactory for any ViewModels that you will have in your project, and also tell Dagger how to use it you can:

Create a Singleton generic ViewModelFactory:

@Singleton
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) :
        ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
            viewModels[modelClass]?.get() as T
}

Create a custom annotation to identify your ViewModels and let Dagger know that it needs to provide them:

@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

Create a new module for your ViewModels:

@Module
abstract class ViewModelModule {

@Binds
internal abstract fun bindsViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

// Add any other ViewModel that you may have
@Binds
@IntoMap
@ViewModelKey(MyViewModel::class)
internal abstract fun bindsMyViewModel(viewModel: MyViewModel): ViewModel
}

Don't forget to declare the new module in your dagger component

And use the view model in your activity, instantiating it with the help of the ViewModelFactory:

class MyFragment : Fragment() {
   @Inject
   lateinit var viewModelFactory: ViewModelProvider.Factory
   lateinit var viewModel: MyViewModel

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
        ...
    }
}
BMacedo
  • 768
  • 4
  • 10
  • Thanks, can I ask further what is the root reason behind for creating a ViewModelFactory for dagger? Why can't dagger just provide the `MyRepository` if `MyRepository` has constructor injection annotation? What's so special in ViewModel that these steps are needed? – Leem Dec 30 '19 at 14:49
  • Now I get error `lateinit property viewModelFactory has not been initialized ` Is it because I should also do the `DaggerAppComponent` builder in `MyFragment` onCreate? Currently it is in `MainActivity` – Leem Dec 30 '19 at 15:11
  • Regarding the new error you got, it is just as you said, you need to inject the component in the fragment in order for @Inject to work. Otherwise the factory will never be initialized. – BMacedo Dec 30 '19 at 18:18
  • The special thing about the ViewModel is that you never call the constructor of the ViewModel yourself. The lifecycle of the viewModel is given internally by the ViewModelProviders. Therefore, if you have dependencies in your VM, you need a ViewModelFactory. And then you use dagger to init that factory. – BMacedo Dec 30 '19 at 18:21
  • I tried injecting the component in the fragment, but same error still... – Leem Dec 30 '19 at 18:22
  • I updated my post again. Is `ViewModelKey ` thing really necessary? If you check @jsamol's answer, he doesn't mention it. – Leem Dec 30 '19 at 20:34