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.
TrailRepository to any Hilt-injected class under test, tying directly back into Unit Testing (JUnit & MockK).