1

I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.

ApplicationComponent

@Singleton
@Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): ApplicationComponent
    }

    fun activityComponent(): ActivitySubComponent.Factory
    fun fragmentComponent(): FragmentSubComponent.Factory

}

FragmentModule

@Module
abstract class FragmentModule {

    @Binds
    @IntoMap
    @ViewModelKey(WeatherViewModel::class)
    abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel

}

ViewModelFactoryModule

@Module
class ViewModelFactoryModule {

    @Provides
    @Singleton
    fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
        return ViewModelFactory(providerMap)
    }
}

Application class

class ThisApplication: Application(),InjectorProvider {

    override fun onCreate() {
        super.onCreate()

        Stetho.initializeWithDefaults(this)
    }

    override val component by lazy {
        DaggerApplicationComponent.factory().create(applicationContext)
    }

}

I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.

InjectorProvider

interface InjectorProvider {
  val component: ApplicationComponent
}

val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component

This is the simple ViewModel I used for testing ViewModel injection.

WeatherViewModel

class WeatherViewModel @Inject constructor(val repository: WeatherRepository): ViewModel() {

    fun printMessage(){
        Log.d("WeatherViewModel","ViewModel binding is working")
        repository.printMessage()
    }

}

Finally, I Injected this view model into a fragment like below.

WeatherFragment

class WeatherFragment : Fragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    override fun onAttach(context: Context) {
        injector.fragmentComponent().create().injectWeatherFragment(this)
        super.onAttach(context)
    }

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

        val mainActivityViewModel =
            ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
        mainActivityViewModel.printMessage()
    }

}

This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.

Edited WeatherViewModel

class WeatherViewModel @AssistedInject constructor(val repository: WeatherRepository,
                                                   @Assisted val id: Int): ViewModel() {

    @AssistedInject.Factory
    interface Factory{ fun create(id: Int) : WeatherViewModel }

    fun printMessage(){
        Log.d("WeatherViewModel","ViewModel binding is working")
        repository.printMessage()
    }
}

Edited ApplicationComponent

@Singleton
@Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): ApplicationComponent
    }

    fun activityComponent(): ActivitySubComponent.Factory
    fun fragmentComponent(): FragmentSubComponent.Factory

}

@AssistedModule
@Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule

From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?

1 Answers1

0

Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.

For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.

class ProfileViewModel @AssistedInject constructor(
  @Assisted val handle: SavedStateHandle,
  @Assisted val navigationDispatcher: NavigationDispatcher,
  private val eventTracker: EventTracker,
  private val getUserUseCase: GetUserUseCase,
  private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
  @AssistedInject.Factory
  interface Factory {
    fun create(
      handle: SavedStateHandle,
      navigationDispatcher: NavigationDispatcher
    ): ProfileViewModel
  }

In Fragment side, all I have to provide in the create method will be the dependencies i marked with @Assisted to fulfil my side of promise.

class ProfileFragment : Fragment() {
  private val navigationDispatcher by getActivityViewModel {
    getBaseComponent().navigationDispatcher
  }
  private val eventTracker by lazy {
    getProfileComponent().eventTracker
  }
  private val viewModel by getViewModel { savedStateHandle ->
    getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
  }

getViewModel is simply an extension function as follows:

inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
  viewModels<T> {
    object : AbstractSavedStateViewModelFactory(this, arguments) {
      override fun <T : ViewModel?> create(
        key: String,
        modelClass: Class<T>,
        handle: SavedStateHandle
      ) = provider(handle) as T
    }
  }
Karan Dhillon
  • 1,186
  • 1
  • 6
  • 14