DEV Community

Cover image for Code clean up using dependency injection with Hilt in Android
Tristan Elliott
Tristan Elliott

Posted on • Edited on

Code clean up using dependency injection with Hilt in Android

Table of contents

  1. What we are doing
  2. The mental model
  3. The Application class
  4. The Hilt components
  5. Hilt bindings
  6. Hilt modules
  7. @Binds
  8. @Provides
  9. Scoping
  10. 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()
Enter fullscreen mode Exit fullscreen mode

}

- To this:
Enter fullscreen mode Exit fullscreen mode

@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 <a name="mental"></a>
- 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:


![diagram of Hilt dependency injection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thwumxx9uri2vdthq6nv.png)

- Essentially Hilt will create [components](https://dagger.dev/hilt/components) and anytime our code needs a dependency, we can tell it to get that dependency from a Hilt component.


### Application class <a name="application"></a>
- 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](https://developer.android.com/training/dependency-injection/hilt-android#application-class):

 `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:
Enter fullscreen mode Exit fullscreen mode

@HiltAndroidApp
class HiltApplication:Application() {

}

- Just make sure that it is declared inside the `AndroidManifest.xml` file:
Enter fullscreen mode Exit fullscreen mode

<application
android:name=".di.HiltApplication"


### Android Entry points <a name="entry"></a>
- 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](https://developer.android.com/training/dependency-injection/hilt-android#android-classes). 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 <a name="comp"></a>
- 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](https://dagger.dev/hilt/components#component-lifetimes)
- So to create a Hilt component we simply apply the annotation:
Enter fullscreen mode Exit fullscreen mode

@HiltViewModel
class DataStoreViewModel(
application:Application,
):AndroidViewModel(application) {

private val tokenDataStore:TokenDataStore = TokenDataStore(application)
val twitchRepoImpl: TwitchRepo = TwitchRepoImpl()
Enter fullscreen mode Exit fullscreen mode

}


### Hilt bindings <a name="binding"></a>
- [Binding](https://developer.android.com/training/dependency-injection/hilt-android#define-bindings) is a term used a lot through out the [documentation](https://developer.android.com/training/dependency-injection/hilt-android). 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` and `twitchRepoImpl` into the primary constructor:
Enter fullscreen mode Exit fullscreen mode

@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](https://developer.android.com/training/dependency-injection/hilt-android#hilt-modules)


### Modules <a name="mod"></a>
- As previously mentioned [Modules](https://developer.android.com/training/dependency-injection/hilt-android#hilt-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 `:
Enter fullscreen mode Exit fullscreen mode

@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 our `ViewModelComponent::class` states this module will be stored in the [ViewModel Component](https://developer.android.com/training/dependency-injection/hilt-android#generated-components).

### Inject interface instances with @Binds <a name="inject"></a>
- 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](https://developer.android.com/training/dependency-injection/hilt-android#inject-interfaces), like so:
Enter fullscreen mode Exit fullscreen mode

@Module
@InstallIn(ViewModelComponent::class)
abstract class ViewModelModule {

@Binds
abstract fun bindsTwitchRepo(
    twitchRepoImpl: TwitchRepoImpl
):TwitchRepo
Enter fullscreen mode Exit fullscreen mode

}

- 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 instantiate `TwitchRepoImpl` 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](https://developer.android.com/training/dependency-injection/hilt-android#component-hierarchy)
- Now that we have the basics we can get a little more complicated

### Inject instances with @Provides <a name="provides"></a>
- 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` or `2) we want to provide more details to Hilt`. Ultimately the code will look like this:

Enter fullscreen mode Exit fullscreen mode

@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)
}
Enter fullscreen mode Exit fullscreen mode

}

- As we have mentioned earlier `@InstallIn(SingletonComponent::class) ` means that all of these dependencies will be stored in the `SingletonComponent`. As per the [component hierarchy](https://developer.android.com/training/dependency-injection/hilt-android#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 <a name="scoping"></a>
- 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 our `SingletonModule` every time Hilt creates an instance `TwitchClient ` or `TokenDataStore ` it will be a new instance. Which is not what we want. In order to fix this, we need to scope our dependencies to the `SingletonComponent`.This is done with the `@Singleton ` annotation. This tells hilt to only create `TwitchClient ` and `TokenDataStore ` 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 one `Retrofit` instance and `TokenDataStore` is a [Datastore](https://developer.android.com/topic/libraries/architecture/datastore) which only allows one instance.

### Resources <a name="resources"></a>
- [Google's dependency injection documentation](https://developer.android.com/training/dependency-injection/hilt-android)
- [Hilt's documentation](https://dagger.dev/hilt/)

### 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](https://twitter.com/AndroidTristan). 

















Enter fullscreen mode Exit fullscreen mode

Top comments (0)