Dagger-Hilt: A Beginner’s Guide to Dependency Injection in Android
Hey there, fellow developer! In this blog i will explain Dagger-Hilt Annotations and Scopes by giving example from real android project.
Next part will be more advance topic about Dagger-Hilt.
What is Dagger-Hilt?
Imagine you are a chef preparing a dish in a busy kitchen. You need many ingredients to make your dish, such as vegetables, meat, spices, and oil. In order to efficiently prepare your dish, you need to have all the ingredients easily accessible and organized.
In the same way, an Android app also needs many “ingredients” or dependencies to function properly, such as database access, network calls, and UI components. Managing all these dependencies can become a complex and time-consuming task, especially as the app grows in size and complexity.
Dependencies are like ingredients that a recipe needs to make a delicious meal. Just like a recipe needs specific ingredients to work correctly, software code also needs specific dependencies to function properly.
This is where Dagger-Hilt comes in. It is a dependency injection framework that automates the process of managing dependencies in an Android app. Just as a chef can rely on a well-organized kitchen to prepare their dish efficiently, an Android app can rely on Dagger-Hilt to manage its dependencies efficiently.
With Dagger-Hilt, developers can easily declare dependencies and specify how they should be injected into different parts of their app. This makes it easier to manage dependencies and reduces the amount of boilerplate code required. As a result, developers can spend more time focusing on building high-quality, user-friendly apps rather than managing dependencies.
Dagger-Hilt is designed to be efficient and performant during compile time. It generates the necessary code at compile time using an annotation processor, which means that it can perform dependency injection more quickly and efficiently than other libraries that rely on reflection at runtime.
By generating code at compile time, Dagger-Hilt also catches errors and issues at compile time, rather than waiting for runtime errors to occur. This can save you a lot of time and effort in debugging and fixing issues.
Dagger-Hilt Annotations
Dagger-Hilt annotations are special words or phrases that are used to help the Dagger-Hilt library work properly in an Android project. They are like secret codes that tell the library what to do and how to do it!
For example, the @Module
annotation is used to create a special class that helps Dagger-Hilt understand how to create and provide instances of different objects, like your networking or database classes.
The @Provides
annotation is used to create a method inside a module that tells Dagger-Hilt how to create a specific object. This is useful when you want to provide an instance of an object that requires some special setup, like a database object.
Different between @Provides and @Module
In Dagger-Hilt, @Provides
and @Module
annotations are used together to help provide Instances of Objects that are required for dependency injection.
@Provides
is an annotation that is used to define a method inside a @Module
class that provides an instance of an object that can be injected. This method is used to specify how to create the object that will be provided.
For example, let’s say you need to provide an instance of a database object to be injected into different parts of your Android app. You might create a @Module
class called DatabaseModule
that provides instances of this database object, like this:
@Module
class DatabaseModule {
@Provides
fun provideDatabase(): AppDatabase {
// code to create and configure the database object
}
}
In this example, the provideDatabase()
method inside the DatabaseModule
class is annotated with @Provides
, which tells Dagger-Hilt that this method should be used to provide an instance of the AppDatabase
object.
Here’s an example of how you can provide a Room database instance using @Module
and @Provides
annotations in Dagger-Hilt:
@Module
@InstallIn(ApplicationComponent::class)
object RoomModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"my-db"
).build()
}
}
In this example, we create a RoomModule
class that is annotated with @Module
. The @Provides
annotation is then used on the provideDatabase()
method to tell Dagger-Hilt that this method provides an instance of the AppDatabase
.
The @ApplicationContext
annotation is used to inject the application context into the method. This is required because Room needs the context to create the database.
The @Singleton
annotation is used to tell Dagger-Hilt to only create a single instance of the AppDatabase
object, which will be shared across the entire application. ((in software design, the Singleton pattern is a creational pattern that ensures only one instance of a particular class is ever created. In other words, it provides a way to create a single instance of an object that can be shared across an entire application.This is useful because it allows you to avoid creating multiple instances of the database))
Finally, the @InstallIn
annotation is used to specify which Dagger-Hilt component this module should be installed in. In this example, we install the RoomModule
in the ApplicationComponent
, which means that this module will be available for injection throughout the entire application.
The @Inject
annotation is used to let Dagger-Hilt know which classes or fields in your app need to be injected with dependencies. This is like saying "Hey, Dagger-Hilt! I need this class or field to be injected with the necessary stuff to work properly!"
You can use the @Inject
annotation to inject the AppDatabase
instance into your classes:
class MyRepository @Inject constructor(
private val database: AppDatabase
) {
// Repository methods that use the AppDatabase instance
}
The @Component
annotation is used to create a special class that helps Dagger-Hilt understand how to put all the different modules together and inject the dependencies into your classes or fields.
And finally, custom scopes and qualifiers can be defined using annotations to help Dagger-Hilt know how to manage the lifecycle of your dependencies and how to distinguish between multiple instances of the same type of dependency.
By using Dagger-Hilt annotations, you can help make your code more organized, easier to maintain, and scalable as your project grows.
In this example, we create a MyRepository
class that uses the @Inject
annotation to tell Dagger-Hilt that this class should be injected with the AppDatabase
instance. We then pass the AppDatabase
instance into the constructor of the class.
Dagger-Hilt Scopes
Scopes are used to manage the lifecycle of objects and define their visibility and availability within the application. A scope is essentially a way of grouping related objects and defining their lifetime.
Managing the lifecycle of objects means ensuring that objects are created and destroyed at the appropriate times during the execution of an application. This is important because objects that are no longer needed can take up valuable memory resources and cause performance issues.
Dagger-Hilt provides a few built-in scopes for managing the lifecycle of objects:
@Singleton
: Objects annotated with@Singleton
are created once and shared across the entire application. This is useful for objects that are expensive to create and can be safely reused throughout the application.(room database instance , instance of the API)@ActivityScoped
: Objects annotated with@ActivityScoped
are created once per activity and are destroyed when the activity is destroyed. This is useful for objects that are related to a specific activity, such as views or presenters.@FragmentScoped
: Objects annotated with@FragmentScoped
are created once per fragment and are destroyed when the fragment is destroyed. This is useful for objects that are related to a specific fragment, such as views or presenters.@ViewScoped
: Objects annotated with@ViewScoped
are created once per view and are destroyed when the view is destroyed. This is useful for objects that are related to a specific view, such as views or presenters.@ServiceScoped
: Objects annotated with@ServiceScoped
are created once per service and are destroyed when the service is destroyed. This is useful for objects that are related to a specific service, such as background tasks or network clients.@ViewModelScoped
: The object will be created once per view model and reused across all fragments and activities that use that view model.
In addition to these built-in scopes, you can also define your own custom scopes using the @Scope
annotation. This allows you to create objects that have a specific lifecycle that is tailored to the needs of your application.
Here’s a real-life example of how scopes can be used in a Dagger-Hilt application:
1.Example
Let’s say you are building an e-commerce application where users can browse and purchase products. The application has two screens: the product catalog screen and the shopping cart screen. The product catalog screen displays a list of available products, while the shopping cart screen displays the items that the user has added to their cart.
To improve the performance and efficiency of the application, you want to ensure that the same instance of the product catalog is used throughout the application, rather than creating a new instance every time the user navigates to the product catalog screen. At the same time, you want to ensure that each instance of the shopping cart is unique to the user, and that it is destroyed when the user logs out of the application.
To accomplish this, you can define two scopes in your Dagger-Hilt application: @Singleton
for the product catalog and @UserScoped
for the shopping cart. The @Singleton
scope ensures that the same instance of the product catalog is used throughout the application, while the @UserScoped
scope ensures that each user has their own unique shopping cart instance. @UserScoped
is a custom scope in Dagger-Hilt that allows you to define objects that are scoped to a particular user. This means that each user of your application will have their own unique instance of the object.
@Module
@InstallIn(ApplicationComponent::class)
object AppModule {
@Singleton
@Provides
fun provideProductCatalog(): ProductCatalog {
return ProductCatalogImpl()
}
@UserScoped
@Provides
fun provideShoppingCart(userRepository: UserRepository): ShoppingCart {
return ShoppingCartImpl(userRepository.getCurrentUser())
}
}
In this example, provideProductCatalog()
is annotated with @Singleton
, which ensures that the same instance of ProductCatalog
is used throughout the application. On the other hand, provideShoppingCart()
is annotated with @UserScoped
, which ensures that each user has their own unique instance of ShoppingCart
.
By using scopes in this way, you can ensure that your application is efficient, responsive, and scalable.
2.Example
Here’s an example of how you can use the @Singleton
scope with Retrofit:
@Singleton
@Module
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
// configure OkHttpClient
return builder.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
In the above code, we annotate the NetworkModule
class with @Singleton
and annotate the provideRetrofit
and provideApiService
methods with @Singleton
. This ensures that only one instance of the Retrofit
service and ApiService
interface is created and maintained throughout the application, and that all parts of the application use the same instance to make HTTP requests.
By using the @Singleton
scope in Retrofit, you can avoid creating multiple instances of the Retrofit service and help optimize the performance of your application.
3.Example
@ViewModelScoped
in a ViewModel
In an Android app, a ViewModel is a component that helps manage data and UI-related logic in a way that’s independent of the lifecycle of an Activity or Fragment. This means that even if the user rotates their device or navigates away from the screen and then comes back, the ViewModel and its data will still be available.
@ViewModelScoped
class MyViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
// ViewModel code
}
In the above code, we annotate the MyViewModel
class with @ViewModelScoped
. This ensures that a single instance of the ViewModel is created and maintained for each associated ViewModelStoreOwner, which typically corresponds to a Fragment or an Activity.
We also inject an instance of UserRepository
into the ViewModel constructor using the @Inject
annotation. The UserRepository
instance will also be scoped to the same ViewModelScope
and will only be created once per ViewModel instance.
By using @ViewModelScoped
, we can ensure that the ViewModel and its dependencies are only created once per ViewModel instance, helping to optimize memory usage and reduce the likelihood of introducing bugs caused by multiple ViewModel instances sharing the same data.
Conclusion
Dagger-Hilt is a powerful dependency injection framework for Android that simplifies the implementation of dependency injection in your Android project. By using Dagger-Hilt, you can easily manage dependencies and promote best practices in your app’s architecture. With its annotations and code generation, Dagger-Hilt reduces boilerplate code and makes it easy to inject dependencies into your classes.
Happy coding!
I want to thank ChatGPT for helping me improve my writing in this blog post . As English is not my first language, it was helpful to clarify my blog post. I hope that the information provided will be helpful to readers and inspire them to further explore this topic.