In the previous tutorials, we created databases and ViewModels manually. We wrote DatabaseProvider singletons. We passed dependencies by hand. It worked — but it doesn’t scale.

When your app has 10 ViewModels, 5 repositories, 3 data sources, and a database — wiring everything manually becomes a nightmare. One missing connection and your app crashes.

Hilt fixes this. You add annotations to your classes, and Hilt connects everything automatically.

What is Dependency Injection?

Dependency injection (DI) means: instead of a class creating its own dependencies, someone else provides them.

Without DI (Manual Wiring)

// ViewModel creates its own database connection
class NotesViewModel(application: Application) : AndroidViewModel(application) {
    // ViewModel knows HOW to create the database
    private val dao = DatabaseProvider.getDatabase(application).noteDao()
}

The ViewModel knows how to create the database. If you change the database setup, you change every ViewModel that uses it.

With DI (Hilt)

// Hilt provides the DAO — ViewModel just declares what it needs
@HiltViewModel
class NotesViewModel @Inject constructor(
    private val dao: NoteDao  // Hilt provides this automatically
) : ViewModel()

The ViewModel says “I need a NoteDao” and Hilt provides one. The ViewModel doesn’t know or care how the database is created.

Why this matters: When you have 10 ViewModels that need a database, you define the database setup ONCE in a Hilt module. Change it once, all 10 ViewModels get the updated version.

What is Hilt?

Hilt is Google’s recommended DI framework for Android. It is built on top of Dagger (a powerful but complex DI library) and makes it much simpler with annotations.

The key annotations:

AnnotationWhereWhat It Does
@HiltAndroidAppApplication classEnables Hilt for the whole app
@AndroidEntryPointActivity/FragmentEnables injection in this screen
@HiltViewModelViewModelEnables injection in this ViewModel
@InjectConstructorTells Hilt “provide these parameters”
@ModuleA classDefines how to create dependencies
@InstallInA moduleDefines the scope (app-wide, activity, etc.)
@ProvidesA function in a moduleTells Hilt how to create something
@SingletonA providerCreate only one instance for the entire app

Setup

Add Hilt dependencies to your project:

// gradle/libs.versions.toml
[versions]
hilt = "2.56.2"
ksp = "2.1.21-2.0.5"

[libraries]
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" }

[plugins]
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
// app/build.gradle.kts
plugins {
    alias(libs.plugins.hilt)
    alias(libs.plugins.ksp)
}

dependencies {
    implementation(libs.hilt.android)
    implementation(libs.hilt.navigation.compose)
    ksp(libs.hilt.compiler)
}

Step 1: Application Class

Create an App.kt with @HiltAndroidApp:

// App.kt — the entry point for Hilt
// This annotation generates the DI container for your entire app

@HiltAndroidApp
class App : Application()

Register it in AndroidManifest.xml:

<application
    android:name=".App"
    ...>

That’s it. This one annotation sets up Hilt for your entire app.

Step 2: Activity

Add @AndroidEntryPoint to your Activity:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                NotesScreen()
            }
        }
    }
}

This tells Hilt: “this Activity can receive injected dependencies.”

Step 3: Define a Module

A module tells Hilt HOW to create things. Here we teach it how to create the Room database and DAO:

// di/AppModule.kt — teaches Hilt how to create dependencies

@Module
@InstallIn(SingletonComponent::class)  // Lives as long as the app
object AppModule {

    @Provides
    @Singleton  // Only one database instance for the entire app
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "notes_database"
        ).build()
    }

    @Provides
    fun provideNoteDao(database: AppDatabase): NoteDao {
        return database.noteDao()
    }
}

Notice: no more DatabaseProvider singleton. Hilt replaces it. The @Singleton annotation ensures only one database instance exists.

How Hilt Reads This

When a ViewModel asks for a NoteDao, Hilt thinks:

  1. “I need a NoteDao. Let me check my modules…”
  2. “Found provideNoteDao() — it needs an AppDatabase
  3. “I need an AppDatabase. Let me check again…”
  4. “Found provideDatabase() — it needs a Context
  5. “I have @ApplicationContext — here’s the context”
  6. “Done. Here’s your NoteDao.”

All automatic. You define the recipes in the module, Hilt follows them.

Step 4: ViewModel with @HiltViewModel

@HiltViewModel
class NotesViewModel @Inject constructor(
    private val dao: NoteDao  // Hilt injects this
) : ViewModel() {

    private val _state = MutableStateFlow(NotesState())
    val state: StateFlow<NotesState> = _state.asStateFlow()

    init {
        observeNotes("")
    }

    fun addNote(title: String, content: String) {
        viewModelScope.launch {
            dao.insert(Note(title = title, content = content))
        }
    }

    fun deleteNote(note: Note) {
        viewModelScope.launch {
            dao.delete(note)
        }
    }

    // ... rest of the ViewModel logic
}

Compare this to the Tutorial #13 version:

// Before Hilt — manual wiring
class NotesViewModel(application: Application) : AndroidViewModel(application) {
    private val dao = DatabaseProvider.getDatabase(application).noteDao()
}

// After Hilt — automatic injection
@HiltViewModel
class NotesViewModel @Inject constructor(
    private val dao: NoteDao
) : ViewModel()  // No AndroidViewModel needed!

Cleaner. No Application parameter. No manual database creation. Just declare what you need.

Step 5: Use in Compose

In Compose, use hiltViewModel() instead of viewModel():

@Composable
fun NotesScreen(
    viewModel: NotesViewModel = hiltViewModel()  // Hilt provides it
) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    // ... your UI code
}

hiltViewModel() comes from the hilt-navigation-compose library. It creates the ViewModel with all dependencies injected.

Before and After: The Full Picture

Before Hilt (Tutorial #13)

DatabaseProvider (singleton — you wrote this)
AppDatabase (you create manually)
NoteDao (you extract manually)
NotesViewModel(application) (needs Application to get database)
NotesScreen(viewModel = viewModel()) (uses AndroidViewModel factory)

After Hilt (Tutorial #14)

@Module AppModule (tells Hilt how to create Database + DAO)
@HiltViewModel NotesViewModel @Inject (Hilt provides NoteDao)
NotesScreen(viewModel = hiltViewModel()) (Hilt handles everything)

What you removed:

  • DatabaseProvider singleton — gone
  • AndroidViewModel — replaced with regular ViewModel
  • Manual database creation — Hilt handles it
  • Application parameter — not needed

Adding Retrofit to the Same Module

Hilt shines when you have multiple dependencies. Add Retrofit alongside Room:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    // --- Room ---

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
    }

    @Provides
    fun provideNoteDao(database: AppDatabase): NoteDao = database.noteDao()

    // --- Retrofit ---

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
            .build()
    }

    @Provides
    fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}

Now any ViewModel can request both:

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val noteDao: NoteDao,    // From Room
    private val userApi: UserApi     // From Retrofit
) : ViewModel()

One module. Multiple dependencies. Zero manual wiring.

Common Mistakes

Mistake 1: Forgetting @AndroidEntryPoint

// BAD — crashes at runtime
class MainActivity : ComponentActivity() { ... }

// GOOD — Hilt can inject into this Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity() { ... }

Mistake 2: Forgetting @HiltAndroidApp

// BAD — Hilt doesn't initialize, everything crashes
class App : Application()

// GOOD — enables Hilt for the entire app
@HiltAndroidApp
class App : Application()

Mistake 3: Using viewModel() Instead of hiltViewModel()

// BAD — ViewModel doesn't get injected dependencies
val viewModel: NotesViewModel = viewModel()

// GOOD — Hilt injects all dependencies
val viewModel: NotesViewModel = hiltViewModel()

Mistake 4: Not Using @Singleton for Databases

// BAD — creates a new database on every injection
@Provides
fun provideDatabase(@ApplicationContext context: Context): AppDatabase { ... }

// GOOD — one database for the entire app
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase { ... }

Quick Reference

AnnotationWhere to UsePurpose
@HiltAndroidAppApplication classEnable Hilt
@AndroidEntryPointActivityEnable injection
@HiltViewModelViewModelEnable ViewModel injection
@Inject constructor()ViewModel/classRequest dependencies
@ModuleObjectDefine dependency recipes
@InstallIn(SingletonComponent)ModuleApp-wide scope
@ProvidesFunction in moduleHow to create a dependency
@SingletonProvider functionOnly create one instance
hiltViewModel()ComposableGet ViewModel with Hilt

Result

Here is what the app looks like when you run the code from this tutorial:

Light ModeDark Mode
Tutorial 14 LightTutorial 14 Dark

Source Code

The complete working code for this tutorial is on GitHub:

View source code on GitHub →

What’s Next?

With Hilt, Room, Retrofit, ViewModel, and Navigation — you now have all the tools for a production-quality Android app. In the next tutorial, we will learn about Animations — making your UI feel smooth and polished.

See you there.