Search code examples
androidkotlindagger-2kotlin-android-extensions

Dagger 2: multi-module project, inject dependency but get "lateinit property repository has not been initialize" error at runtime


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?


Solution

  • 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]
            ...
        }
    }