Dependency Injection with Hilt

Throughout this track, classes like TrailRepository have needed other objects to do their job — a TrailDao for local storage, a TrailApiService for the network. If TrailRepository constructed those itself internally, every place that needed a TrailRepository would have to duplicate the exact setup logic for building a working TrailDao and TrailApiService, and swapping in a fake version of either for a test (see Unit Testing) would mean rewriting TrailRepository itself. Dependency injection is the general fix: a class declares what it needs, and something else outside it is responsible for actually constructing and handing over those dependencies.

Hilt: Dagger's Boilerplate, Handled For You

Hilt is Google's official dependency injection library for Android, and it's built directly on top of Dagger, the same underlying dependency injection engine used in the Java track's raw Dagger 2 lesson. The relationship matters: Dagger itself is powerful but notably verbose to set up by hand — components, subcomponents, and scopes all had to be wired together manually. Hilt generates that entire scaffold for you automatically, driven by a small, focused set of annotations, while Dagger still does the real dependency-graph work underneath. The result is a genuinely large usability improvement over raw Dagger, without giving up any of Dagger's compile-time safety.

The Entry Point: @HiltAndroidApp

Hilt needs exactly one place to start building its dependency graph: TrailLog's Application class, annotated once.

@HiltAndroidApp
class TrailLogApplication : Application()

This single annotation triggers Hilt's code generation for the whole app — nothing else needs to be added to this class for Hilt itself to function.

Injecting Into a Fragment: @AndroidEntryPoint

Any Activity or Fragment that needs Hilt-provided dependencies gets marked with @AndroidEntryPoint. Once marked, requesting a Hilt-aware ViewModel is a single delegate call — Hilt wires up that ViewModel's own dependencies behind the scenes, without TrailListFragment needing to know or care what they are:

@AndroidEntryPoint
class TrailListFragment : Fragment(R.layout.fragment_trail_list) {

    private val viewModel: TrailListViewModel by viewModels()

    // viewModel is fully ready to use here — Hilt already constructed it
    // and supplied its TrailRepository dependency automatically
}

Compare this to manually building a TrailListViewModel with a ViewModelProvider.Factory written by hand — by viewModels() with Hilt in place needs none of that boilerplate. TrailListViewModel itself just needs @HiltViewModel on its class and @Inject constructor, and Hilt handles the rest.

Classes Hilt Can't Build Alone: @Module / @InstallIn

Hilt can construct a class directly if it understands how — but it has no idea how to build a Room database or a Retrofit instance on its own; those need explicit configuration (a database name, a base URL) that only your code knows. A @Module tells Hilt how to provide exactly that kind of instance, and @InstallIn says which part of the app's lifetime it should live for:

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

    @Provides
    @Singleton
    fun provideTrailDatabase(@ApplicationContext context: Context): TrailDatabase {
        return Room.databaseBuilder(context, TrailDatabase::class.java, "traillog.db")
            .build()
    }

    @Provides
    @Singleton
    fun provideTrailApiService(): TrailApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.traillog.example.com/")
            .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
            .build()
            .create(TrailApiService::class.java)
    }
}

@InstallIn(SingletonComponent::class) means these instances live as long as the app process does — one shared TrailDatabase and one shared TrailApiService, handed out to anything that asks for them. @Provides marks each function as a recipe Hilt should follow whenever something requests that exact type.

Classes Hilt CAN Build Directly: @Inject constructor()

For a class like TrailRepository, whose constructor parameters are themselves already things Hilt knows how to provide (the module above, plus anything else Hilt-managed), no @Module is needed at all — just mark the constructor:

class TrailRepository @Inject constructor(
    private val trailDao: TrailDao,
    private val trailApiService: TrailApiService
) {
    suspend fun getTrails(): List<Trail> = trailDao.getAllTrails()
}

Hilt sees @Inject constructor, notices it needs a TrailDao and a TrailApiService, finds both already registered (the DAO from TrailDatabase, the API service from the module above), and assembles a TrailRepository automatically wherever one is requested — including inside TrailListViewModel's own @Inject constructor.

Scoping: @Singleton and Narrower Lifetimes

@Singleton, seen above, means exactly one shared instance for the whole app process — appropriate for a database or network client that's genuinely expensive to create and safe to share. Hilt also supports narrower scopes tied to a shorter lifetime, such as one instance per Activity or per Fragment, for dependencies that shouldn't outlive the screen that created them. Choosing the right scope is mostly about matching a dependency's actual lifetime — TrailLog's database connection should clearly outlive any single screen, so @Singleton is the right call there.

Note: Every annotation and class shown here is correct, real Hilt/Dagger code as it would appear in an actual Android Studio project — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.
Worth knowing: Hilt also makes supplying fake dependencies for tests genuinely straightforward — a test module can be installed in place of the real one, handing a fake TrailRepository to any Hilt-injected class under test, tying directly back into Unit Testing (JUnit & MockK).