Dependency Injection (DI) provides objects with their dependencies from the outside instead of creating them internally. On Android, Hilt (built on Dagger) is the recommended DI framework — it generates boilerplate code and integrates with Activities, Fragments, and ViewModels.
Without DI With Hilt
──────────── ─────────
ViewModel creates Hilt injects
└── Repository └── Repository
└── RetrofitClient └── Retrofit (singleton)
Tight coupling Loose coupling, easy to test
Benefit
Description
Testability
Swap real Repository with fake in unit tests
Reusability
Share Retrofit, Room, Repository across app
Maintainability
Change implementation in one module
Lifecycle
Hilt scopes match Activity, ViewModel, Singleton
1Dependency Injection Basics
A dependency is any object a class needs to function — a Repository in a ViewModel, Retrofit in a Repository, or a DAO in a Repository. DI passes these in rather than letting classes construct them directly.
Problem — tight coupling
Kotlin — Without DI (bad)class TaskViewModel : ViewModel() {
// ViewModel creates its own dependency — hard to test, hard to swap
private val repository = TaskRepository(
api = RetrofitClient.api,
dao = AppDatabase.getInstance().taskDao()
)
fun loadTasks() {
viewModelScope.launch {
repository.getTasks()
}
}
}
Solution — constructor injection
Kotlin — With DI (good)class TaskViewModel(
private val repository: TaskRepository // injected from outside
) : ViewModel() {
fun loadTasks() {
viewModelScope.launch {
repository.getTasks()
}
}
}
class TaskRepository(
private val api: ApiService,
private val dao: TaskDao
) {
suspend fun getTasks(): Result<List<Task>> { /* ... */ }
}
Hilt simplifies Dagger for Android. Add plugins and dependencies, annotate your Application and Activities, and Hilt generates the dependency graph at compile time.
Kotlin — MyApplication.kt@HiltAndroidApp
class MyApplication : Application()
XML — AndroidManifest.xml<application
android:name=".MyApplication"
... >
Annotate Activity and Fragment
Kotlin — MainActivity.kt@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: TaskViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
@AndroidEntryPoint
class TaskFragment : Fragment() {
private val viewModel: TaskViewModel by viewModels()
}
Hilt setup checklist
Add Hilt plugin and dependencies (with kapt)
Create @HiltAndroidApp Application class
Register Application in AndroidManifest
Add @AndroidEntryPoint to Activities and Fragments
Create @Module classes for third-party objects
Use @HiltViewModel on ViewModels
3Modules
Hilt modules tell Hilt how to create objects you don't own — Retrofit instances, OkHttp clients, Room databases, and third-party library classes. Use @Provides in a @Module class.
Kotlin — Bind interface to implementation// Use @Binds when you have an interface + implementation (more efficient than @Provides)
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindTaskRepository(
impl: TaskRepositoryImpl
): TaskRepository
}
interface TaskRepository {
suspend fun getTasks(): Result<List<Task>>
}
class TaskRepositoryImpl @Inject constructor(
private val api: ApiService,
private val dao: TaskDao
) : TaskRepository {
override suspend fun getTasks(): Result<List<Task>> { /* ... */ }
}
4Inject Annotation
The @Inject annotation tells Hilt how to create an instance of a class. Mark constructors for automatic injection, or use field injection for Android framework classes that Hilt cannot construct.
Constructor injection — preferred
Kotlin — @Inject constructorclass TaskRepository @Inject constructor(
private val api: ApiService,
private val dao: TaskDao
) {
suspend fun getTasks(): Result<List<Task>> {
return try {
val remote = api.getTasks()
dao.insertAll(remote)
Result.success(remote)
} catch (e: Exception) {
Result.failure(e)
}
}
}
@HiltViewModel
Kotlin — ViewModel with @Inject@HiltViewModel
class TaskViewModel @Inject constructor(
private val repository: TaskRepository
) : ViewModel() {
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
init {
loadTasks()
}
fun loadTasks() {
viewModelScope.launch {
repository.getTasks()
.onSuccess { _tasks.value = it }
}
}
}
// In @AndroidEntryPoint Activity:
private val viewModel: TaskViewModel by viewModels()
Field injection — Android classes
Kotlin — Field injection in Activity@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var analyticsTracker: AnalyticsTracker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// analyticsTracker is injected by Hilt before onCreate
analyticsTracker.logScreenView("MainActivity")
}
}
@ApplicationContext and @ActivityContext
Kotlin — Inject Context safelyclass PreferencesManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
fun saveToken(token: String) {
prefs.edit().putString("auth_token", token).apply()
}
}
class FragmentHelper @Inject constructor(
@ActivityContext private val context: Context
) {
fun showDialog(message: String) {
AlertDialog.Builder(context).setMessage(message).show()
}
}
What Hilt can and cannot inject
Class Type
Injection Method
Your Repository, UseCase
@Inject constructor
ViewModel
@HiltViewModel @Inject constructor
Activity, Fragment
@AndroidEntryPoint + field injection
Retrofit, Room, OkHttp
@Module @Provides
Interface → Implementation
@Binds in abstract module
5Singleton Pattern
A singleton ensures only one instance of a class exists app-wide. Hilt's @Singleton scope replaces manual double-checked locking — Retrofit, Room, and Repository instances are typically singletons.
Manual singleton vs Hilt
Kotlin — Manual singleton (avoid with Hilt)// Old pattern — boilerplate, hard to test
class RetrofitClient private constructor() {
companion object {
@Volatile
private var instance: Retrofit? = null
fun getInstance(): Retrofit {
return instance ?: synchronized(this) {
instance ?: buildRetrofit().also { instance = it }
}
}
}
}
Kotlin — Hilt @Singleton (preferred)@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit { /* ... */ }
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Hilt creates one instance for the entire app lifetime
class TaskRepository @Inject constructor(
private val api: ApiService // same singleton everywhere
)
@Singleton on @Inject class
Kotlin — Singleton class with @Inject@Singleton
class UserSessionManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE)
fun saveUserId(id: Int) = prefs.edit().putInt("user_id", id).apply()
fun getUserId(): Int = prefs.getInt("user_id", -1)
fun clearSession() = prefs.edit().clear().apply()
}