How to inject bundle arguments to ViewModel
If you need to inject arguments that have just been passed to the Activity/Fragment directly to the ViewModel you can use the help of dagger to achieve this.
Using @BindsInstance you can specify an abstract function in the component following the builder pattern, and then override seedInstance to build with the value:
@Subcomponent.Builder
abstract class Builder: AndroidInjector.Builder<RatesFragment>() {
abstract override fun build(): RatesComponent
@BindsInstance
abstract fun currency(currency: String): Builder
override fun seedInstance(fragment: RatesFragment) {
currency(fragment.getCurrencyFromBundle())
}
}
Note: This is equivalent to passing the arguments through the module’s constructor and then providing it through @Provides annotation. This is more common when using dagger’s older versions but the builder approach is more efficient and cleaner.
Dagger to ViewModel:
We’ve successfully passed the currency argument from the fragment to dagger. Next step: inject to view model.
To have a ViewModel with custom constructor (where you would pass arguments or interactors or anything else), you need a view model factory. In this factory we basically take the injected objects from dagger and tell it, the factory, to create the view model with these objects instead of the default constructor-less ViewModel :
public class RatesViewModelFactory implements ViewModelProvider.Factory {
@NonNull
private final GetRatesList getRatesList;
@NonNull
private final String currency;
@Inject
public RatesViewModelFactory(@NonNull final GetRatesList getRatesList, @NonNull final String currency) {
this.getRatesList = getRatesList;
this.currency = currency;
}
@NonNull
@Override
public RatesViewModel create(@NonNull final Class modelClass) {
return new RatesViewModel(getRatesList, currency);
}
In the Activity/Fragment, you would inject the factory and initialise the view model like this:
class RatesFragment: BaseFragment() {
@Inject lateinit var viewModelFactory: RatesViewModelFactoryoverride fun initializeViewModelAndObserve() {
viewModel = ViewModelProviders.of(this, viewModelFactory).get(RatesViewModel::class.java)}
There you go. Your view model’s constructor may look like this now:
class RatesViewModel constructor(getRatesList: GetRatesList,
currency: String): ViewModel() {...}
Note: You may think you can just provide the argument through the module directly instead of using seedInstance because dagger (2.11+) injects the Activity/Fragment to its graph. Like this:
@Provides
String providesCurrency(RatesFragment fragment) {
return fragment.getCurrencyFromBundle();
}
Unfortunately that’s not going to work because it creates a circular dependency and leaves dagger boggled. Why? RatesFragment has the RatesViewModelFactory as a dependency inside it (because it needs to initialise the view model). But the RatesViewModelFactory has the currency injected inside it which comes from RatesFragment. Dagger literally lost it when I did so. Better not.
Of course you might say: hey, why not just pass the argument this way?:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?{
super.onViewCreated(view, savedInstanceState)
viewModel.hereIsYourArgument(getCurrencyFromBundle())
}
Well of course this would do the job but it’s cleaner to inject rather than pass through an argument. With injection, we know that this currency is really a dependency to the view model. Meaning, the view model can’t start doing its job without it. Moreover, the UI has stepped further away from the business logic, which means we can easily test the ViewModel with the currency value mocked inside it.