5

I want to inject a dependency (HomeViewModel) into my fragment (HomeFragment).

I have a class (HomeViewModelImpl) which implemented that abstraction (HomeViewModel) and inside this class, I'm overriding parent's methods of course.

The abstraction class (HomeViewModel) is an abstract class which extended from BaseViewModel.

The BaseViewModel is a normal open class in which extended from ViewModel class from the Android lifecycle component.

The problem is I got an error when I want to inject HomeViewModel into the fragment:

> error: [Dagger/MissingBinding] [dagger.android.AndroidInjector.inject(T)] com.example.mvvm.ui.home.HomeViewModel cannot be provided without an @Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.example.mvvm.MyApplication> {
            ^
  com.example.mvvm.ui.home.HomeViewModel is injected at
      com.example.mvvm.ui.home.HomeFragment.viewModel
  com.example.mvvm.ui.home.HomeFragment is injected at
      dagger.android.AndroidInjector.inject(T)

HomeFragment:

class HomeFragment : BaseFragment() {
//Error comes from this line
@Inject
lateinit var viewModel: HomeViewModel
}

HomeViewModel:

//If I write @Inject annotation here, the error goes away,
//but then I have to remove the abstract keyword, then I have to open the class
//and the useful usage of that abstract class in HomeViewModelImpl class
//will be gone, and I have to set open keyword on the HomeViewModel and
//on its method.
/*open*/ abstract class HomeViewModel /*@Inject constructor()*/ : BaseViewModel() {

sealed class State {
    data class AlbumsLoaded(val albums: List<AlbumData>) : State()
    object ShowLoading : State()
    object ShowContent : State()
    object ShowError : State()
}

abstract fun fetchAlbums()
}

BaseViewModel:

open class BaseViewModel : ViewModel() {

private val compositeDisposable: CompositeDisposable = CompositeDisposable()

protected fun addDisposable(disposable: Disposable) {
    compositeDisposable.add(disposable)
}

private fun clearDisposables() {
    compositeDisposable.clear()
}

override fun onCleared() {
    clearDisposables()
}
}

HomeModule:

@Module(includes = [
//HomeModule.HomeViewModelProvide::class,
HomeModule.HomeVM::class])
internal abstract class HomeModule {

@ContributesAndroidInjector
internal abstract fun homeFragment(): HomeFragment

@Module
abstract class HomeVM {
    @Binds
    @IntoMap
    @ViewModelKey(HomeViewModelImpl::class)
    internal abstract fun bindHomeViewModel(viewModel: HomeViewModelImpl): HomeViewModel
//I've changed the return type of this method from HomeViewModel to
//BaseViewModel and ViewModel, but error still exists!
}

//I've written this to provide HomeViewModel, but compiler shows another error
//that says there is a dependency circle!
/*@Module
class HomeViewModelProvide {
    @Provides
    internal fun provideHomeViewModel(homeViewModel: HomeViewModel): HomeViewModel = homeViewModel
}*/
}

ViewModelKey:

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

ViewModelFactory:

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

override fun <T : ViewModel> create(modelClass: Class<T>): T {
    var creator: Provider<out ViewModel>? = creators[modelClass]
    if (creator == null) {
        for ((key, value) in creators) {
            if (modelClass.isAssignableFrom(key)) {
                creator = value
                break
            }
        }
    }
    if (creator == null) {
        throw IllegalArgumentException("unknown model class $modelClass")
    }
    try {
        @Suppress("UNCHECKED_CAST")
        return creator.get() as T
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}
}

ViewModelModule:

@Module
internal abstract class ViewModelModule {

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

BaseModule:

@Module
internal abstract class BaseModule {

@ContributesAndroidInjector(modules = [HomeModule::class])
internal abstract fun mainActivity(): MainActivity
}

AppComponent:

@Singleton
@Component(modules = [
AndroidSupportInjectionModule::class,
ViewModelModule::class,
AppModule::class,
BaseModule::class
])
interface AppComponent : AndroidInjector<MyApplication> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<MyApplication>()
}

Note: Please read inline comments on above snippet codes.

All I want is set HomeViewModel as an abstract class and inject it where I want.

Onik
  • 19,396
  • 14
  • 68
  • 91
Dr.jacky
  • 3,341
  • 6
  • 53
  • 91

3 Answers3

4

The solution is to create a middle abstract class between the child and the real parent, then the child has to extend from that middle abstract class.

HomeViewModel & the Middle Abstract class:

open class HomeViewModel @Inject constructor() : BaseViewModel() {

    sealed class State {
        data class AlbumsLoaded(val albums: List<AlbumData>) : State()
        object ShowLoading : State()
        object ShowContent : State()
        object ShowError : State()
    }

    abstract class Implementation : HomeViewModel() {
        abstract fun fetchAlbums()
    }
}

HomeViewModelImpl:

class HomeViewModelImpl : HomeViewModel.Implementation() {

    override fun fetchAlbums() { }
}

HomeFragment:

class HomeFragment : BaseFragment() {

    @Inject
    lateinit var viewModel: HomeViewModel
}

Source: https://stackoverflow.com/a/18331547/421467

Onik
  • 19,396
  • 14
  • 68
  • 91
Dr.jacky
  • 3,341
  • 6
  • 53
  • 91
2

If you're trying to inject a child of an abstract base class, you need to let dagger know how to create that instance. That's done via a method on a module that returns an instance of the type and has an @Provides annotation. That class will be called every time it needs to create an instance of the class (if you only wish 1 instance of it, you can also annotate it with a scope annotation like @Singleton).

The reason this is necessary is because the class you're trying to make is abstract. It can't be instantiated directly, so Dagger can't do its normal thing of calling the @Inject constructor or default constructor.

Gabe Sechan
  • 90,003
  • 9
  • 87
  • 127
  • No, I'm trying to inject the abstract class itself(`HomeViewModel`), not the child of the abstract base class(`HomeViewModelImpl`). – Dr.jacky Sep 21 '18 at 17:20
  • You can't inject a abstract class because you can't create an instance of it – Gabe Sechan Sep 21 '18 at 17:40
  • I know it Gabe; I just want to know is there any other way to implement the `HomeViewModel` without `abstract` keyword, but force the `HomeViewModelImpl` to implement the parent's methods (`fetchAlbums`)? – Dr.jacky Sep 21 '18 at 17:43
  • 2
    Remove the abstract keyword and have the default implementation throw an UnsupportedOperationException, so the child has to override it or it will throw an exception when called. – Gabe Sechan Sep 21 '18 at 17:48
  • Are you talking about *Checked Exceptions*? Maybe I didn't get your point but I'm using Kotlin, and there is no such this.(https://kotlinlang.org/docs/reference/exceptions.html#checked-exceptions). Would you please explain a little bit more with snippet code? – Dr.jacky Sep 21 '18 at 18:00
2

It's all here.

Dagger cannot create an instance of class without @Inject constructor(...). On the other hand in Java/Kotlin you cannot create an instance of abstract class and Dagger "uses" Java/Kotlin in this case.

An option you have is to extend HomeViewModel and inject the child instance with Dagger or make HomeViewModel not abstract.

Onik
  • 19,396
  • 14
  • 68
  • 91