Table of contents
- What we are doing
- The mental model
- The Application class
- The Hilt components
- Hilt bindings
- Hilt modules
- @Binds
- @Provides
- Scoping
- Resources
The code
Introduction
- I have embarked on my next app, a Twitch client app. This series will be all my notes and problems faced when creating this app.
Getting started
- I wont spend any time on setting up the dependencies for Hilt. You can find that in the documentation, HERE
What we are doing
- Through the power of dependency injection with Hilt, we will take our code from looking like this:
class DataStoreViewModel(
application:Application,
):AndroidViewModel(application) {
private val tokenDataStore:TokenDataStore = TokenDataStore(application)
val twitchRepoImpl: TwitchRepo = TwitchRepoImpl()
}
- To this:
@HiltViewModel
class DataStoreViewModel @Inject constructor(
private val twitchRepoImpl: TwitchRepo,
private val tokenDataStore:TokenDataStore
): ViewModel() {
}
- Not only does our code look cleaner but it will also be easier to test.
The mental model
- If you are unfamiliar with dependency injections, then it might be hard to actually grasp what Hilt is doing. Which is why I like to us this mental model:
- Essentially Hilt will create components and anytime our code needs a dependency, we can tell it to get that dependency from a Hilt component.
Application class
- Assuming we both now have the proper dependencies, the next step is to annotate a class with
@HiltAndroidApp
. As it is stated in the documentation:
All apps that use Hilt must contain an Application class that is annotated with @HiltAndroidApp. @HiltAndroidApp triggers Hilt's code generation, including a base class for your application that serves as the application-level dependency container.
- We can create this class like so:
@HiltAndroidApp
class HiltApplication:Application() {
}
- Just make sure that it is declared inside the
AndroidManifest.xml
file:
<application
android:name=".di.HiltApplication"
Android Entry points
To allow Hilt to inject dependencies into our code, we must annotate our classes with specific annotations. A full list of annotations can be found HERE. But since we want to inject code into a ViewModel we need to annotation said ViewModel with
@HiltViewModel
.IMPORTANT : it is important to note that when you annotate a class with a Hilt annotation, you must also annotate all the classes that rely on it with the appropriate annotation. Which means if we annotate a ViewModel with
@HiltViewModel
, then we must annotate the Fragment with@AndroidEntryPoint
and the surrounding activity with@AndroidEntryPoint
.
The components
- Annotating our classes with
@HiltViewModel
and@AndroidEntryPoint
generates an individual Hilt component for each annotated Android class. These components will have a lifecycle tied directly to the class they are annotating. A detailed diagram explaining the lifecycles can be found HERE - So to create a Hilt component we simply apply the annotation:
@HiltViewModel
class DataStoreViewModel(
application:Application,
):AndroidViewModel(application) {
private val tokenDataStore:TokenDataStore = TokenDataStore(application)
val twitchRepoImpl: TwitchRepo = TwitchRepoImpl()
}
Hilt bindings
-
Binding is a term used a lot through out the documentation. So lets define it, we can think of a binding as an object that informs Hilt how it should create our dependencies. We tell hilt to use a
binding
by adding@Inject constructor
to the primary constructor. - We can now move
tokenDataStore
andtwitchRepoImpl
into the primary constructor:
@HiltViewModel
class DataStoreViewModel @Inject constructor(
private val twitchRepoImpl: TwitchRepo,
private val tokenDataStore:TokenDataStore
): ViewModel() {
}
This might look nice but it does not work yet and that is because we have not defined any bindings to tell Hilt how to create these dependencies.
depending on your needs simply adding Hilt and adding the
@Inject constructor
annotation may be all you need. Try running your app and see if it crashes or not. If your app crashes then you need to create more specific bindings and we can do so through Hilt modules
Modules
- As previously mentioned Modules are used to create more specific bindings, which Hilt will use to create our dependencies. To create a module we need to create a new class and annotate it with 2 specific annotations,
@Module
and@InstallIn
:
@Module //defines class as a module
@InstallIn(ViewModelComponent::class)
abstract class ViewModelModule {
}
- The
@InstallIn
annotation is used to define which Hilt component our module is installed in. These modules will then provide Hilt with information on how to create bindings. For ourViewModelComponent::class
states this module will be stored in the ViewModel Component.
Inject interface instances with @Binds
- The whole point of a module is to give Hilt the appropriate information so that it can create bindings which is then used to create an instance of the appropriate dependency. For my code I want Hilt to inject an interface,
TwitchRepo
. We can inject interfaces with @Binds, like so:
@Module
@InstallIn(ViewModelComponent::class)
abstract class ViewModelModule {
@Binds
abstract fun bindsTwitchRepo(
twitchRepoImpl: TwitchRepoImpl
):TwitchRepo
}
- With the
@Binds
annotation we create an abstract function and give it two pieces of information:
1)The function return type : This tells Hilt what interface our function provides instances of.
2)The function parameter : This tells Hilt which implementation to provide.
- With this new dependency that we have created inside our module we have told Hilt that anytime a ViewModel needs an instance of
TwitchRepo
it should instantiateTwitchRepoImpl
and pass it to our code. Notice How I stated,That anytime a ViewModel
, this dependency is only available in a ViewModel due to the Component hierarchy - Now that we have the basics we can get a little more complicated
Inject instances with @Provides
- Along with
@Binds
we can also use another annotation called@Provides
inside of our modules. Generally we will use the provides over binds annotation for two reasons.1) we do not own the class we want Hilt to instantiate
or2) we want to provide more details to Hilt
. Ultimately the code will look like this:
@Module
@InstallIn(SingletonComponent::class)
object SingletonModule {
@Singleton
@Provides
fun providesTwitchClient(): TwitchClient {
return Retrofit.Builder()
.baseUrl("https://api.twitch.tv/helix/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(TwitchClient::class.java)
}
@Singleton
@Provides
fun providesTokenDataStore(
@ApplicationContext appContext: Context
): TokenDataStore {
return TokenDataStore(appContext)
}
}
- As we have mentioned earlier
@InstallIn(SingletonComponent::class)
means that all of these dependencies will be stored in theSingletonComponent
. As per the component hierarchy this means our these dependencies will be available to all of our code. Now we need to talk a little about how this is scoped
Scoping
In documentation and blog posts you will constantly see the quote:
By default, all bindings in Hilt are unscoped. This means that each time your app requests the binding, Hilt creates a new instance of the needed type.
So even in ourSingletonModule
every time Hilt creates an instanceTwitchClient
orTokenDataStore
it will be a new instance. Which is not what we want. In order to fix this, we need to scope our dependencies to theSingletonComponent
.This is done with the@Singleton
annotation. This tells hilt to only createTwitchClient
andTokenDataStore
once and reuse the same instance.It is worth pointing out that the
@Singleton
annotation should only be used if it is necessary for your code to function, which it is for mine. I only want oneRetrofit
instance andTokenDataStore
is a Datastore which only allows one instance.
Resources
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)