Android ViewModel injection with Dagger

Categories: Development
Abstract black lines on a light backgroundAbstract black lines on a light background
Profile photo of Aleksandrs Orlovs
Aleksandrs Orlovs
Jul 24, 2018
4 min

ViewModel from Android Architecture Components is a long-awaited solution from Google to address android application architecture, and it packs some really nice features too. A fine new addition to the set of essential android development tools that we use here at Chili (alongside Dagger, RxJava, Data Binding Library). But making the new kid to play nice with the others is not always as easy as it seems.

The structure of this article reflects my thought process (with questions and ideas that arose along the way) and results in a final solution of how to make ViewModel work well with dependency injection using Dagger.

You should be familiar with Dagger for Android to understand the basic setup and some behind the scenes actions.

Basic setup

An android application that uses:

  • Standard Dagger setup (which generates subcomponents for it’s fragments to use with AndroidInjection), with one exception: since fragments are used as independent pieces of functionality, they are injected from Application class directly (implements HasSupportFragmentInjector). This gives us an opportunity to inject after fragment super.onCreate(savedInstanceState) call. Which is crucial to allow android to retrieve previously created ViewModel before we will try to inject it.
  • A ViewModel that requires a new instance of some expensive object to be constructed.

Ideally, we would like for Dagger to deal with the creation and injection of a ViewModel instance.

/* Feature fragment */ class FeatureFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AndroidSupportInjection.inject(this) } } /* Module for feature specific stuff */ @Module abstract class FeatureModule { @ContributesAndroidInjector abstract fun bind(): FeatureFragment } /* Root application component */ @Component(modules = [ AppModule::class, FeatureModule::class ]) @Singleton interface AppComponent /* Root application module */ @Module class AppModule { @Provides fun provideExpensive() = Expensive() } /* ViewModel we want to inject in FeatureFragment */ class FeatureViewModel(val expensive: Expensive) : ViewModel()

First, let’s outline the core concepts.

Lifecycle and scope

ViewModel instances are retained on fragment recreation. This makes their scope span from the moment user enters a part of your application (provided by this fragment) and until user leaves it. During this scope (lets call it “user scope”) fragment can get recreated more than once due to configuration changes (i.e. device rotation). For such scenarios, ViewModel is a good place to keep the state.

Every time a new fragment is created (even if it was just recreated due to configuration change), we perform dependency injection. Under the hood, it’s done by creating a new subcomponent, that was generated for this fragment, and using it to provide dependencies. Subcomponent is then discarded and we have no way of keeping it for later reuse. As a result, there is no sane way of a subcomponent being scoped with our “user scope”, meaning we can’t simply replace ViewModelProviders with scoped Dagger providers for our ViewModel needs.

Factory

A workaround is for Dagger to provide ViewModelProvider.Factory instances. They will contain all required dependencies for ViewModel creation. Although, during a single “user scope” a new factory instance will be injected each time, the actual ViewModel instance will only be created once, by the first factory that was provided.

class FeatureViewModel(val expensive: Expensive) : ViewModel() { /* Factory for creating FeatureViewModel instances */ class Factory(val expensive: Expensive) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return FeatureViewModel(expensive) as T } } } class FeatureFragment : Fragment() { /* Each fragment will get a new factory instance */ @Inject lateinit var vmFactory: FeatureViewModel.Factory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AndroidSupportInjection.inject(this) // Use injected factory instance with ViewModelProviders val vm = ViewModelProviders.of(this, vmFactory)[FeatureViewModel::class.java] } } @Module abstract class FeatureModule { @ContributesAndroidInjector(modules = [ ProvideViewModel::class // Install module in the generated subcomponent ]) abstract fun bind(): FeatureFragment /* Module that provides factory and is installed in the generated subcomponent */ @Module class ProvideViewModel { @Provides fun provideFeatureViewModelFactory(expensive: Expensive) = FeatureViewModel.Factory(expensive) } }

Providers

Looks nice, but there is a problem. Dependencies required to create ViewModel are injected into factory as concrete instances. Some of these dependencies might be expensive to create, and since only one factory instance will actually be used during a single “user scope”, this means that we are wasting resources on objects that will never be used. We can minimise the impact by using injecting factory with providers instead of concrete instances. This way dependencies for ViewModel will be provided only once, when instance is created. Even cleaner, we can define a provider method for a ViewModel, and use generated provider as in factory.

class FeatureViewModel(val expensive: Expensive) : ViewModel() { class Factory(val provider: Provider<FeatureViewModel>) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return provider.get() as T // Delegate call to provider } } } @Module abstract class FeatureModule { @ContributesAndroidInjector(modules = [ ProvideViewModel::class ]) abstract fun bind(): FeatureFragment @Module class ProvideViewModel { /* Standart module function to provide instances of FeatureViewModel */ @Provides fun provideFeatureViewModel(expensive: Expensive) = FeatureViewModel(expensive) /* Receive FeatureViewModel provider and pass it to factory */ @Provides fun provideFeatureViewModelFactory(provider: Provider<FeatureViewModel>) = FeatureViewModel.Factory(provider) } }

Now we have a nice and clean way to define ViewModel creation in a Dagger module. No instances of ViewModel or any of it dependencies will be provided until a factory requests it. So during the recreation and injection we only waste Factory object instances.

Singleton abstract factory

We know that the scope of a ViewModel is larger than that of a fragment subcomponent. It means that we should add a module responsible for providing ViewModel factory to parent component instead of fragment subcomponent. This will make all provided factories in the project to have the same scope in the same component, as the result, we can combine them all into a singleton factory. To accomplish that, we can use Dagger multi-bindings.

/* Key used to associate ViewModel types with providers */ @MapKey @Target(AnnotationTarget.FUNCTION) annotation class ViewModelKey( val value: KClass<out ViewModel> ) @Module class AppModule { @Provides fun provideExpensive() = Expensive() /* Singleton factory that searches generated map for specific provider and uses it to get a ViewModel instance */ @Provides @Singleton fun provideViewModelFactory( providers: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>> ) = object : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return requireNotNull(providers[modelClass as Class<out ViewModel>]).get() as T } } } /* Install module responsible for providing ViewModel into parent component */ @Module(includes = [ FeatureModule.ProvideViewModel::class ]) abstract class FeatureModule { /* No specific module for generated subcomponent */ @ContributesAndroidInjector abstract fun bind(): FeatureFragment @Module class ProvideViewModel { /* Associate this provider method with FeatureViewModel type in a generated map */ @Provides @IntoMap @ViewModelKey(FeatureViewModel::class) fun provideFeatureViewModel(expensive: Expensive): ViewModel = FeatureViewModel(expensive) } } class FeatureFragment : Fragment() { /* Each fragment will get the same factory instance */ @Inject lateinit var vmFactory: ViewModelProvider.Factory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AndroidSupportInjection.inject(this) val vm = ViewModelProviders.of(this, vmFactory)[FeatureViewModel::class.java] } }

We no longer create new instances of factories on injection, which means we don’t produce any waste.

Final touch

Now, we can make a final change and really tuck all implementation into the modules. The subcomponent (that is generated using android injection) binds instance of the target fragment. This allows us to use this fragment’s instance for injections within the scope of the subcomponent. We can add a new module to our subcomponent and move the call from the fragment to the ViewModelProviders.

class FeatureFragment : Fragment() { /* Fragment doesn't know any details about how FeatureViewModel instances are created */ @Inject lateinit var vm: FeatureViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AndroidSupportInjection.inject(this) } } @Module(includes = [ FeatureModule.ProvideViewModel::class ]) abstract class FeatureModule { /* Install module into subcomponent to have access to bound fragment instance */ @ContributesAndroidInjector(modules = [ InjectViewModel::class ]) abstract fun bind(): FeatureFragment /* Module that uses bound fragment and provided factory uses ViewModelProviders to provide instance of FeatureViewModel */ @Module class InjectViewModel { @Provides fun provideFeatureViewModel( factory: ViewModelProvider.Factory, target: FeatureFragment ) = ViewModelProviders.of(target, factory).get(FeatureViewModel::class.java) } @Module class ProvideViewModel { @Provides @IntoMap @ViewModelKey(FeatureViewModel::class) fun provideFeatureViewModel(expensive: Expensive): ViewModel = FeatureViewModel(expensive) } }

Finally, all logic for ViewModel creation is moved to Dagger modules.

Wrapping up

And that’s about it. Comments and suggestions are welcome. Feel free to share your thoughts or solutions about this topic and also ideas about what you would like to read in my next post.

Thank you for reading :)

UPDATE: Example project showcasing this approach https://github.com/ChiliLabs/viewmodel-dagger-example

https://miro.medium.com/max/300/1*hB1OofoTpqI6wm43oKjH8A.png

Receive a new Mobile development related stories first. — Hit that follow button

Facebook: Chili Labs

Twitter: @ChiliLabs

www.chililabs.io

Share with friends