Mastering Dependency Injection in Android with Hilt
In the evolving world of Android development, writing scalable and maintainable code is essential. One of the best ways to achieve this is through Dependency Injection (DI). Among various DI frameworks, Hilt has emerged as the go-to solution for Android developers due to its seamless integration with Android components. In this article, we will explore Hilt, its core components like @Bind, @Provides, @AndroidEntryPoint, @Module, and Context Qualifiers, and how they simplify dependency injection.
What is Hilt?
Hilt is a dependency injection library built on top of Dagger, tailored specifically for Android. It reduces boilerplate code and manages dependencies efficiently, allowing developers to focus on building better apps. Hilt integrates smoothly with Android’s lifecycle components and handles complex dependency graphs with ease.
Why use Hilt?
• Simplified Dependency Injection: Hilt automates much of the setup required for dependency injection.
• Lifecycle Awareness: It’s designed to work with Android’s lifecycle-aware components like Activities, Fragments, and ViewModels.
• Scalability: Hilt makes scaling apps easier by managing dependencies efficiently.
- Integration with Jetpack Components: It works seamlessly with Jetpack libraries, enhancing app performance and structure.
Core Concepts of Hilt
1. @AndroidEntryPoint
The @AndroidEntryPoint annotation is the bridge between Android components and Hilt. It tells Hilt to provide the necessary dependencies for the component.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var analyticsService: AnalyticsService
}
Here, AnalyticsService is automatically injected when MainActivity is created. Without @AndroidEntryPoint, Hilt cannot inject dependencies into Android components.
2. @Module and @InstallIn
A Module in Hilt is used to define how to provide dependencies. The @Module annotation tells Hilt that this class provides dependencies, and @InstallIn specifies the component scope.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
}
}
@Module: Marks the class as a Hilt module.
• @InstallIn(SingletonComponent::class): Keeps the dependency alive for the entire app lifecycle.
- @Provides: Supplies the actual dependency.
3. @Provides vs. @Binds
@Provides
Use @Provides when you need to manually construct an object.
@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
@Provides
fun provideAuthService(): AuthService {
return AuthServiceImpl()
}
}
@Binds
Use @Binds when you are providing an implementation for an interface.
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
}
Key Differences:
• @Provides: For custom object creation logic.
- @Binds: For interface-to-implementation binding (more efficient, less boilerplate).
4. Context Qualifiers
When injecting Context, it’s essential to specify which context to inject: Application Context or Activity Context. Hilt provides built-in qualifiers for this.
class PreferenceManager @Inject constructor(
@ApplicationContext private val context: Context
)
• @ApplicationContext: Provides the global application context.
• @ActivityContext: Provides the current activity’s context.
This helps avoid memory leaks and ensures that the correct context is used.
Scoping in Hilt
Hilt offers different scopes to manage the lifecycle of dependencies:
• SingletonComponent → Application-wide singleton.
• ActivityRetainedComponent → Survives configuration changes.
• ActivityComponent → Tied to Activity lifecycle.
• FragmentComponent → Tied to Fragment lifecycle.
• ViewModelComponent → Scoped to ViewModel.
- ServiceComponent → Tied to Service lifecycle.
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@Provides
fun provideSampleService(): SampleService {
return SampleService()
}
}
Real-World Example
Let’s combine everything with a real-world scenario: Injecting a UserRepository into a ViewModel.
Step 1: Create Repository Interface and Implementation
interface UserRepository {
fun getUserData(): String
}
class UserRepositoryImpl @Inject constructor() : UserRepository {
override fun getUserData() = "User Data Loaded"
}
Step 2: Bind Interface to Implementation
@Module
@InstallIn(ViewModelComponent::class)
abstract class UserRepositoryModule {
@Binds
abstract fun bindUserRepository(
userRepositoryImpl: UserRepositoryImpl
): UserRepository
}
Step 3: Inject into ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
fun fetchUserData() = userRepository.getUserData()
}
Step 4: Use in Activity
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("UserData", viewModel.fetchUserData())
}
}
Best Practices with Hilt
1. Use @Binds for Interfaces: It generates more efficient code than @Provides.
2. Scope Dependencies Wisely: Overusing singletons can lead to memory issues.
3. Avoid Injecting Context Directly: Use @ApplicationContext or @ActivityContext.
4. Modularize Dependencies: Break modules into smaller, reusable pieces.
Conclusion
Hilt is a game-changer for Android development. It simplifies dependency injection, reduces boilerplate, and improves app scalability. By mastering annotations like @AndroidEntryPoint, @Module, @Provides, @Binds, and Context Qualifiers, you can write cleaner, more efficient, and maintainable Android applications.
Implement Hilt in your next project and experience the difference in code quality and productivity!
Ready to simplify your Android projects with Hilt?
Share your thoughts and experiences in the comments below!