A ViewModel needs a repository, which needs an API and a database. Dependency injection wires this graph for you instead of constructing it by hand. Learn DI and Hilt, the standard on Android.
As layers stack up, constructing objects by hand becomes painful: to make a ViewModel you must build its repository, which needs an API client and a database, each with their own dependencies. Manual wiring couples everything and makes testing hard. Dependency injection inverts this — objects declare what they need, and a container provides it — so you assemble the graph in one place and swap pieces (like a fake API in tests) freely.
Manual construction couples everything and repeats setup:
val db = buildDatabase(context)
val api = buildRetrofit().create(UserApi::class.java)
val repo = DefaultUserRepository(api, db.userDao())
val viewModel = HomeViewModel(repo) // wire this by hand EVERYWHERE
Dependency injection flips it: a class DECLARES its dependencies (constructor
params), and a container PROVIDES them. You define how to build each thing once;
DI assembles the graph. Swapping a real API for a fake (tests) is trivial.Hilt is the standard DI framework for Android, built on Dagger. You annotate a class’s constructor with @Inject and Hilt learns how to build it; annotate the Application with @HiltAndroidApp to bootstrap. Then Hilt can construct your whole object graph automatically wherever it is needed. Constructor injection — asking for dependencies as constructor parameters — is the core pattern.
@HiltAndroidApp
class MyApp : Application() // bootstraps Hilt once, app-wide
// Hilt knows how to build these because their constructors are @Inject-annotated
class DefaultUserRepository @Inject constructor(
private val api: UserApi,
private val dao: UserDao
) : UserRepository { /* ... */ }
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: UserRepository // Hilt provides it automatically
) : ViewModel() { /* ... */ }
// no manual wiring — Hilt builds the graph from these annotations.Hilt cannot @Inject a constructor it does not control — a Retrofit instance, a Room database, an interface-to-implementation binding. You teach it with a module: @Provides functions that build these, and @Binds to map an interface to its implementation. Scopes like @Singleton control lifetime, so the database and network client are created once and shared.
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides @Singleton
fun provideApi(): UserApi =
Retrofit.Builder().baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(UserApi::class.java)
@Provides @Singleton
fun provideDb(@ApplicationContext ctx: Context): AppDatabase =
Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()
@Provides fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}
// map interface -> implementation
@Module @InstallIn(SingletonComponent::class)
abstract class RepoModule {
@Binds abstract fun bindUserRepo(impl: DefaultUserRepository): UserRepository
}With the graph defined, you mark an Activity @AndroidEntryPoint and obtain Hilt-provided ViewModels in Compose with hiltViewModel(). Hilt is compile-time verified (errors surface at build time) but adds annotation processing; Koin is a lighter, pure-Kotlin DI alternative that resolves at runtime with less setup. Knowing both lets you pick per project — Hilt is the Google-recommended default.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Hilt provides the ViewModel + its whole dependency graph
val viewModel: HomeViewModel = hiltViewModel()
HomeScreen(viewModel)
}
}
}
// Hilt: compile-time verified, more setup (annotation processing).
// Koin: lighter, pure-Kotlin, runtime resolution, less boilerplate.
// val repo: UserRepository by inject() // Koin style
// Hilt is Google's recommended default; Koin is a popular lightweight option.