An exploration into the Hilt library introduced in Dagger 2.28. Keep in mind, that at the time of this writing (5/29/2020), Hilt is still in alpha, and no docs have been released other than the sparse Javadocs, so my interpretations may be erroneous. Feel free to let me know if I've got anything wrong by filing an issue or messaging me on Reddit (/u/Pzychotix).
Edit: Hilt documentation is now up! Please refer to the official documentation instead of this repo.
Table of contents generated with markdown-toc
The first annotation required to use Hilt. This must be placed on your Application
class.
@HiltAndroidApp
class MainApplication : Application()
The annotation placed on your Activity/Fragment/Services/etc. This is roughly equivalent to the DaggerActivity/DaggerFragment classes, which handled calling the relevant injection methods for you.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: Repository
....
}
Notably, this supports Views while the old dagger-android injection did not (at least not out of the box).
This is the new way to add modules to a component. Rather than declaring the module as a part of the
@Component
annotation, @InstallIn
allows you to declare which component to add this module to.
@Module
@InstallIn(ApplicationComponent::class)
object MainAppModule {
@Provides
fun repository() = Repository()
}
There are many new base Components defined in hilt, which seem to now be the components you will be using. Like before, child components will inherit their parent's bindings, so anything available at the parent level will also be available in the children. In the above code samples, the MainAppModule is installed at the Application level, and can be injected into the Activity.
From what I can gather, Hilt automatically uses these components without any further declaration,
so installing a module in FragmentComponent
would make that module available to all Hilt enabled fragments.
I'm not sure if there's any way to change what component is used, which would mean that providing different implementations of an interface in different components would be impossible. This sounds like a downgrade from dagger-android, so I'd need further investigation to make sure this is true.
Roughly equivalent to provision methods.
Since we can't modify the components now, we can install an @EntryPoint
interface, which will make the target component
conform to the interface.
// This EntryPoint makes the ApplicationComponent implement the RepositoryLocator interface.
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface RepositoryLocator {
fun getRepository(): Repository
}
We can then use the EntryPointAccessors utility class to access the component through the interface.
EntryPointAccessors.fromApplication(applicationContext, RepositoryLocator::class.java).getRepository()
Note that unlike module bindings, entry point interfaces are not inherited by their subcomponents, so an EntryPoint installed
on an ApplicationComponent can only be accessed using the fromApplication
accessor.
I avoid using provision methods in my projects, preferring to have these things injected directly, but if you use them currently, this would be how you migrate.
A library module can now install into the app module's Component without any interaction needed at the app module level.
// Declared in the feature module
@Module
@InstallIn(ApplicationComponent::class)
object FeatureModule {
@Provides
fun feature() = Feature()
}
// main app can be injected with Feature
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var feature: Feature
....
}
This is a nice quality of life change from dagger-android, where a library module would need to manually be added to the app's graph. Potentially a little sketch though, if 3rd party libraries can abuse this to latch onto your graph. It'd be an interesting attack vector, at the very least.
On the testing side, Hilt has really great support compared to barebones Dagger. It provides some simple ways to replace a dependency, whereas Dagger by itself requires quite a bit of organization to do the same.
Thanks to @remcomokveld for kicking this off by adding an example Hilt test.
Like the name says, this removes a module that's been add with @InstallIn
. This is super useful for replacing production dependencies with a test version by installing your own test module.
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@UninstallModules(FeatureModule::class)
class HiltExampleTest {
@Module
@InstallIn(ApplicationComponent::class)
object FeatureModule {
@Provides
fun feature(): Feature = TestFeature()
}
}
The resulting component is still compile-time checked, so if you uninstall a module
without replacing the needed dependencies (or install a new one without uninstalling the old),
you'll get the standard Dagger errors to cover for you. Additionally, both the @UninstallModules
and the above @InstallIn
are local to the test class, so they don't affect what you do in other test classes.
@BindValue
and others
As an alternative to creating and installing a new module for a test dependency, you can use the @Bind
family
of annotations to directly add it to the component. This is also important if you want to interact with the test dependency
during the test. Keep in mind that Dagger binds by exact type, and this is no different.
@HiltAndroidTest
@UninstallModules(FeatureModule::class)
class HiltExampleTest {
private val testFeature = TestFeature()
@BindValue
@JvmField
val feature: Feature = testFeature
}
A neat thing that can be done with these test level binds is that they can easily replace classes
that are not provided through modules, but through the constructor @Inject
annotation.
While module overriding is common in Dagger for test overrides, this is something new that's very convenient.
Your tests can be injected as well! They'll grab the dependencies from the component that's configured for that test class.
@HiltAndroidTest
class SomeTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit val dependency: Dependency
@Before
fun beforeEach() {
hiltRule.inject()
...
}
}
On the AndroidX side, they've been hard at work at integrating Hilt into some specific components. They're currently available as snapshot builds from the androidx repo:
implementation "androidx.hilt:hilt-work:1.0.0-SNAPSHOT"
implementation "androidx.hilt:hilt-common:1.0.0-SNAPSHOT"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-SNAPSHOT"
kapt "androidx.hilt:hilt-compiler:1.0.0-SNAPSHOT"
Thanks to @R4md4c for putting in the legwork on all this!
The Hilt-Work artifact provides automatic generation of the common multi-bind worker factory pattern (if you don't know what this is, then now you won't have to!) It will find all workers annotated with @WorkerInject
and create a HiltWorkerFactory
that can be used with your WorkManager to inject your workers.
Simply annotate your Workers with @WorkerInject
and @Assisted
for the Context
and WorkerParameters
in the constructor. Any additional injected dependencies can just be added into the constructor.
class RepositoryWorker @WorkerInject constructor(
private val repository: Repository,
@Assisted private val context: Context,
@Assisted private val workerParameters: WorkerParameters
) : Worker(context, workerParameters)
To finish the Hilt-Work setup, you just need to attach it to your WorkManager like so:
@HiltAndroidApp
class MainApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder().setWorkerFactory(workerFactory).build()
}
Very similar to the Hilt-Work artifact, this artifact handles all the multi-map binding for ViewModel creation.
Simply annotate your ViewModel with @ViewModelInject
. The constructor can take in any dependencies available at the @ActivityRetainedComponent
scope, as well as an optional SavedStateHandle marked with an @Assisted
annotation.
class MainViewModel
@ViewModelInject
constructor(
private val repository: Repository,
@Assisted private val savedStateHandle: SavedStateHandle
): ViewModel()
None! As long as the ViewModel is retrieved from an @AndroidEntryPoint
annotated Activity/Fragment, it will automatically handle getting the injected ViewModel.