18.
Hilt & Architecture Components
Written by Massimo Carli
In the previous chapter, you learned how to migrate the Busso App from Dagger Android to Hilt. You saw the main Hilt architectural decisions and how to apply them using new APIs like @InstallIn and @AndroidEntryPoint.
In this chapter, you’ll learn more about Hilt, including how to:
- Use Hilt with other supported Android standard components, like
Services. - Create custom Hilt
@Endpoints for currently unsupported Android standard components, likeContentProviders. - Use Hilt with ViewModels.
Throughout the chapter, you’ll work with the RayTrack app. Don’t worry about the amount of code the app has. It’s purposely a complex app, to give you the opportunity to work with Hilt’s architectural components. You’ll only focus on the things that have to do with Hilt. To save space, you’ll only see the most relevant parts of the code in this chapter.
Note: An interesting exercise would be to simplify RayTrack’s code. Check the project for the full code.
Note: This chapter requires knowledge of the main Android Architecture Components, like Lifecycle, ViewModel and LiveData.
The RayTrack app
In this chapter, you’ll work on RayTrack. Use Android Studio to open the RayTrack project in the starter folder for this chapter. You’ll see the project structure shown in Figure 18.1:
RayTrack has some modules in common with Busso:
- libs.di.scope
- libs.location.api
- libs.ui.navigation
It also has some new modules:
- libs.location.api-android
- libs.location.flow
These are just a different implementation of the abstraction you have in libs.location.api for Android. They use Kotlin Flows and coroutines.
Build and run and you’ll get, after the splash screen and permission request, something like in Figure 18.2:
Yeah, the UI isn’t the best, but the RayTrack app does what you need. :] When you press the Start Tracking button, you start a Foreground Service that keeps track of your current location and stores it in a database. Pressing the Stop Tracking button while the tracking is running stops the service. While the service is running, you can also see the location in the notification area:
Selecting the notification area returns you to the list of locations shown in Figure 18.2. Finally, you can select the Clear Data button and delete the database content at any time.
Now that you know how RayTrack works, it’s time to see how it’s set up.
RayTrack’s architecture
RayTrack consists of three main parts:
- Foreground service: Tracks the location.
- RayTrackContentProvider: Provides location data persistence.
- MainActivity: Displays the location data on the screen.
Here’s a detailed description of each of these important parts.
Tracking location data in the foreground
RayTrack uses Service to track the user’s location. Because the user’s location is sensitive data, Android requires you to implement this as a foreground service so the user always knows when it’s running on their device.
This means that you have some specific constraints you must follow. For instance, you must display a Notification when Service is running. In Figure 18.4, you can see the architecture for this part of the RayTrack app:
In the diagram, there are some important things to note:
-
TrackingServiceextendsLifecycleService, which is a utility class Google provides for cases when you need yourServiceto be aLifecyclerOwner. You need this becauseTrackerStateManagerexposes an interface based on LiveData that requiresLifecyclerOwnerto be observed. - You start and stop
TrackingServicedirectly fromMainActivityusing the classicstartService()andstopService(). - To display the notification that Android requires when
TrackingServiceis running, you need a dependency onNotificationManager. - An Android
Serviceis an adapter between the Android environment and some thread that needs to live for a certain time. It’s the ideal place to start long-living threads. In this case,TrackingServicestarts and stopsTrackerand observesTrackerStateManagerto update the information inNotification. -
Trackeris the abstraction of the object responsible for the actual tracking of the location.
Looking at the diagram in Figure 18.4, you also understand which objects TrackingService depends on. It needs a reference to:
TrackerTrackerStateManager
The good news is that you can use Service as an injection target for Hilt. To inject the required dependencies, you just need to do what’s already in TrackingService.
Injecting dependencies
Open TrackingService.kt in raytracker.service and look at the following code:
@ExperimentalCoroutinesApi
@AndroidEntryPoint // 1
class TrackingService : LifecycleService() {
@Inject
lateinit var tracker: Tracker // 2
@Inject
lateinit var trackerStateManager: TrackerStateManager // 2
// ...
}
In that code, you used:
-
@AndroidEntryPointbecauseServiceis an Android Standard Component that Hilt supports. -
@Injectfor the property containing the reference to the dependent objects.
To understand where those dependencies come from, just click on the icons in Figure 18.5:
In Tracker’s case, you’ll see the following code from TrackerModule.kt in di:
interface TrackerModule {
@Module(
includes = [FlowLocationModule::class]
)
@InstallIn(ServiceComponent::class) // HERE
interface ServiceBindings {
@Binds
fun bindTracker(
impl: TrackerImpl
): Tracker
}
// ...
}
This shows how to use @InstallIn to install the Tracker implementation in the dependency graph for the ServiceComponent.
Now, open TrackerImpl.kt in service and look at the following code:
@ServiceScoped // HERE
class TrackerImpl @Inject constructor(
private val trackerStateManager: TrackerStateManager,
private val locationFlow: @JvmSuppressWildcards Flow<LocationEvent>
) : Tracker, CoroutineScope {
// ...
}
This is an example of @ServiceScoped as the @Scope for an object that lives as long as a Service.
Select the icon next to TrackerStateManager and you end up, once again, in TrackerModule.kt in di, where you have the following code:
interface TrackerModule {
// ...
@Module
@InstallIn(ApplicationComponent::class) // HERE
interface ApplicationBindings {
@Binds
fun bindTrackerStateManager(
impl: TrackerStateManagerImpl
): TrackerStateManager
}
}
In this case, TrackerStateManager needs to be in ApplicationComponent because it will contain the current state of the tracking. Look at TrackerStateManagerImpl.kt in state and see the following code:
@ApplicationScoped // HERE
class TrackerStateManagerImpl @Inject constructor(
@ApplicationContext private val context: Context
) : TrackerStateManager {
// ...
}
Here you just use @ApplicationScoped, which is the same @AliasOf for @Singleton that you used in the previous chapters.
TrackingService is an example of how to use Hilt with a Service. But what about a standard component that isn’t supported yet? You’ll see how to do that next.
Persisting location data
In the previous paragraph, you saw how to inject dependencies in TrackingService. You discovered that the dependency injection in a Service with Hilt is no different from the injection in an Activity, a Fragment or any other component Hilt supports.
You still use @AndroidEntryPoint to tag the Service as an injection target and @Inject to assign values to local properties. Service is an Android standard component that Hilt supports — but you’re not always so lucky. At the moment, for example, Hilt doesn’t support ContentProviders. However, Hilt gives you some tools to fix the problem.
To see how everything works you need to:
- Understand
ContentProvider’s role in RayTrack. - Enable the dependency injection in
ContentProviderusing@EntryPoint.
In the first case, a good UML diagram will help.
Understanding the role of ContentProvider in RayTrack
RayTrack uses a ContentProvider to persist location data. To understand how this works, look at the diagram in Figure 18.6:
This diagram explains some interesting points:
-
TrackStateManageris the abstraction of the object responsible for maintainingTracker’s state. This is the one that tells you ifTrackeris running and what the user’s current location is. -
TrackerStateManagerImplis theTrackStateManagerimplementation. Every time it receives a newTrackState, it delegates the persistence of the relatedTrackDatato aTrackDataHelper. -
TrackDataHelperis the abstraction of the object responsible forTrackData’s persistence. -
TrackDataHelperImplis theTrackDataHelperimplementation that usesContentResolverto persist the data into aContentProviderinRayTrackContentProvider. -
RayTrackContentProviderdelegates the persistence operation to aTrackDaoyou define using the architecture component Room.
Open RayTrackContentProvider.kt in repository.contentprovider and notice how you only need to manage the dependency on the Room database:
class RayTrackContentProvider : ContentProvider(), CoroutineScope {
// ...
private lateinit var trackDatabase: TrackDatabase
private lateinit var trackDao: TrackDao
override fun onCreate(): Boolean {
trackDatabase = getRoomDatabase()
trackDao = trackDatabase.trackDao()
return true
}
private fun getRoomDatabase(): TrackDatabase = // HERE
Room.databaseBuilder(
context!!,
TrackDatabase::class.java,
Config.DB.DB_NAME
).fallbackToDestructiveMigration().build()
// ...
}
At the moment, the relationship is a composition because you create the TrackDatabase instance in RayTrackContentProvider directly.
It would be nice to remove getRoomDatabase() and inject TrackDatabase directly using Hilt. You’ll see how to do that next.
Creating a custom @EntryPoint
As you learned, Hilt doesn’t support ContentProviders as a predefined @AndroidEntryPoint — but it gives you tools to work around that. That’s what you’ll do for RayTrackContentProvider.
Note: In this case, you just need to inject
TrackDatabase. This is a component that lives as long as the app, so it’s inApplicationComponent.
In this case, you need to:
- Add the binding for
TrackDatabasein the proper@Component. - Define an
@EntryPointthat declaresTrackDatabaseas an object you can access from an unsupported component. - Access and use the object
@EntryPointexports inRayTrackContentProvider.
Adding bindings
To start, open TrackDBModule.kt in di and add:
@Module
@InstallIn(ApplicationComponent::class)
object TrackDBModule {
@Provides
fun provideTrackDatabase( // HERE
@ApplicationContext context: Context
): TrackDatabase =
Room.databaseBuilder(
context,
TrackDatabase::class.java,
Config.DB.DB_NAME
).build()
// ...
}
Here, the TrackDBModule installs bindings into ApplicationComponent. You’re just providing TrackDatabase by using code similar to the one you had in RayTrackContentProvider.
Now, TrackDatabase is in the dependency graph for the app. You need to use it from RayTrackContentProvider.
Defining an entry point
Next, open RayTrackContentProvider.kt in repository.contentprovider and add the following code:
class RayTrackContentProvider : ContentProvider(), CoroutineScope {
@EntryPoint // 1
@InstallIn(ApplicationComponent::class) // 2
interface ContentProviderEntryPoint {
fun trackDatabase(): TrackDatabase // 3
}
// ...
}
This is the definition of the ContentProviderEntryPoint interface that declares what RayTrackContentProvider needs from RayTrack’s existing dependency graph. In this code, you used:
-
@EntryPointto tell Hilt, and then Dagger, that this is the interface you want your custom component to use to access objects in the dependency graph. -
@InstallIn(ApplicationComponent::class)to make the@EntryPointHilt creates for you as part ofApplicationComponent. -
trackDatabase()to tell Hilt, and then Dagger, that the object you need isTrackDatabase.
Accessing TrackDatabase
Finally, you need to access TrackDatabase in RayTrackContentProvider. In RayTrackContentProvider.kt in repository.contentprovider, replace the existing getRoomDatabase() with:
private fun getRoomDatabase(): TrackDatabase {
val appContext = context?.applicationContext ?: throw IllegalStateException() // 1
val hiltEntryPoint =
EntryPointAccessors.fromApplication( // 2
appContext,
ContentProviderEntryPoint::class.java) // 3
return hiltEntryPoint.trackDatabase() // 4
}
In this code, you:
- Access
ApplicationContext, throwing an exception ifContextisn’t available. - Use
fromApplication()static function ofEntryPointAccessorsto access the reference to the@EntryPointyou defined in the same class. - Need to provide the class for
ContentProviderEntryPointto get an object of the right type. - Use
ContentProviderEntryPointto access theTrackDatabasereference.
It’s important to note that EntryPointAccessors is a utility class that Hilt provides for cases where the bindings you need are in @Components that Hilt already supports. Other methods are:
EntryPointAccessors.fromActivity()EntryPointAccessors.fromFragment()EntryPointAccessors.fromView()
In your case, you used EntryPointAccessors.fromApplication() because the binding for TrackDatabase is in ApplicationComponent.
Note: To be picky, what you just did is not actually dependency injection. You still depend on the
EntryPointAccessorsand the injection doesn’t come from outside. This is reminiscent of what happens with the service locator pattern.
It’s important to say that, in ContentProvider’s case, you access a TrackDatabase object that has a binding in ApplicationComponent. As you’ll see later, Hilt gives you the tools to create a custom @Component with a custom lifecycle. To do this properly, you need control over the creation and destruction of the custom @Component instance. This is something you don’t have for ContentProvider. This isn’t a problem, however, because you can assume that its lifecycle is the same as ApplicationComponent’s.
Great job! You managed to create a custom @EntryPoint for a ContentProvider you implemented in RayTrackContentProvider. Build and run and check that everything works as expected.
It’s now time to see how TrackData displays onscreen.
Displaying the location data
RayTrack can display the location data on the screen. The diagram in Figure 18.7 describes the current architecture:
In this diagram, you see some interesting points:
-
MainActivitydepends onTrackerStateManager,TrackDataHelperImplandCurrentLocationViewModel. -
CurrentLocationViewModeldepends onTrackDataHelperandTrackerStateManager.
The code in MainActivity.kt in ui.main shows something that the diagram already highlights:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var trackerStateManager: TrackerStateManager // 2
lateinit var locationViewModel: CurrentLocationViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
locationViewModel = CurrentLocationViewModel( // 1
this.application,
trackerStateManager,
TrackDataHelperImpl(this) // 3
)
createNotificationChannel()
startStopButton = findViewById(R.id.startStopTrackingButton)
trackDataRecyclerView = findViewById(R.id.location_recyclerview)
initRecyclerView(trackDataRecyclerView)
handleButtonState(locationViewModel.locationEvents().value)
handleTrackDataList(locationViewModel.storedLocations().value)
}
// ...
}
Here, MainActivity:
- Creates the instance of
CurrentLocationViewModel. As you learned in the first chapter of this book, this is a composition relationship, which you know isn’t the best. - Depends on
TrackerStateManagerbecauseCurrentLocationViewModelconstructor needs it. - Creates an instance of
TrackDataHelperImplas the implementation ofTrackDataHelperto pass toCurrentLocationViewModel.
There’s definitely room for improvement here.
Your goal now is to inject CurrentLocationViewModel into MainActivity, as you do with other components. Hilt provides a new library to accomplish this. You just need to:
- Add the dependency to the Hilt
ViewModelsupport library. - Annotate your
ViewModelimplementation with@ViewModelInject. - Inject your
ViewModelusingViewModelProvideror theviewModels()extension.
Now, you can migrate and replace the composition relationship with a simple dependency.
Adding VieModel support for Hilt
To add ViewModel support for Hilt, you need to extend the annotation processor. Open build.gradle for app and add the following dependencies:
// ...
dependencies {
// ...
// ViewModel Hilt support
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$viewmodel_hilt_version" // 1
kapt "androidx.hilt:hilt-compiler:$viewmodel_hilt_version" // 2
// ...
}
Note:
viewmodel_hilt_versionis already defined in versions.gradle. The current value is1.0.0-alpha02but you can update it if you need to.
Here, you just add the:
- Dependency to the hilt-lifecycle-viewmodel library.
- Extension to the annotation processor.
Now, sync Gradle for your project and you’re ready to use @ViewModelInject in your code.
Using @ViewModelInject in your ViewModel implementation
Open CurrentLocationViewModel.kt in ui.main and look at the following header:
@ExperimentalCoroutinesApi
class CurrentLocationViewModel(
application: Application,
private val trackerStateManager: TrackerStateManager,
private val trackDataHelper: TrackDataHelper
) : AndroidViewModel(application) {
// ...
}
This defines all the CurrentLocationViewModel dependencies. Now, change it to the following:
@ExperimentalCoroutinesApi
class CurrentLocationViewModel @ViewModelInject constructor( // HERE
application: Application,
private val trackerStateManager: TrackerStateManager,
private val trackDataHelper: TrackDataHelper
) : AndroidViewModel(application) {
// ...
}
Here, you just added @ViewModelInject to the primary constructor. In your case, all the parameters are already in the dependency graph for the app so you won’t get any complaints when you build.
Injecting your ViewModel implementation in MainActivity
For your final step, you need to improve MainActivity. Open MainActivity.kt in ui.main and replace it with:
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//@Inject // REMOVE 1
//lateinit var trackerStateManager: TrackerStateManager
val locationViewModel: CurrentLocationViewModel by viewModels() // 2
private lateinit var trackListAdapter: TrackListAdapter
private lateinit var trackDataRecyclerView: RecyclerView
private lateinit var startStopButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* // REMOVE 2
locationViewModel = CurrentLocationViewModel(
this.application,
trackerStateManager,
TrackDataHelperImpl(this) // 2
)
*/
createNotificationChannel()
startStopButton = findViewById(R.id.startStopTrackingButton)
trackDataRecyclerView = findViewById(R.id.location_recyclerview)
initRecyclerView(trackDataRecyclerView)
handleButtonState(locationViewModel.locationEvents().value)
handleTrackDataList(locationViewModel.storedLocations().value)
}
// ...
}
Here, you:
- Removed the
trackerStateManagerinjection because you don’t need it anymore. Hilt and Dagger will inject it inCurrentLocationViewModelfor you. - Deleted the code to create
CurrentLocationViewModel. This includes the creation ofTrackDataHelperImpl, which Hilt and Dagger will also inject inCurrentLocationViewModel. - Used Kotlin delegation with
viewModels(). It’s smart enough to understand whatlocationViewModel’s type is, so it knows whichViewModelto inject.
Now, build and run to check that everything works. The diagram in Figure 18.8 gives you a useful description of what you just did:
As you can see, this is much better because:
-
MainActivityonly depends onCurrentLocationViewModel. -
CurrentLocationViewModeldepends on theTrackerStateManagerandTrackDataHelperabstractions.
Great job! You just removed all the composition dependencies and replaced them with loosely coupled dependencies.
Creating a custom @Component with @DefineComponent
Earlier, you learned that Hilt doesn’t support all the Android standard components as @AndroidEntryPoints. That’s why you had to use @EntryPoint with RayTrackContentProvider. In that case, you wanted to inject TrackDatabase, which is an object in ApplicationComponent, with @Singleton — or your alias, @ApplicationContext — scope.
However, Hilt provides the APIs to create your own @Component with a specific @Scope and its own lifecycle. It’s worth mentioning that this is not something you should do very often because the existing @Components already cover most of the use cases.
A possible example is the logged state for a user in an app. You might have objects that need to be there only when the user is logged in to the app, and which you should remove if the user isn’t logged in.
In RayTrack’s case, you’ll define a new @Component for objects that need to exist only when the Tracker is running and there’s something to display. This means that the @Component should exist only when there’s an Activity to display locations from a running Tracker.
The steps you need to follow are:
- Create a custom
@Scope. - Create a custom
@Componentusing@DefineComponent. - Add a
@DefineComponent.Builder. - Manage the lifecycle for the
@DefineComponent. - Add bindings to the custom
@Componentwith@EntryPoint. - Use the custom
@Componentin your code.
It’s time to dive in.
Creating a custom @Scope
Each @Component Hilt supports has a specific @Scope, so the custom @Component you’ll create needs one as well. You already know how to do this. Just create a new package named custom in di and, inside, add a new file named TrackRunningScoped.kt with the following code:
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class TrackRunningScoped
This is nothing different from what you did for other custom @Scopes in the previous chapters.
Creating a custom @Component using @DefineComponent
The next step is to create the custom @Component. In the same di.custom package, create a new file named TrackRunningComponent.kt and add the following code:
@DefineComponent(parent = ActivityComponent::class) // 1
@TrackRunningScoped // 2
interface TrackRunningComponent
In these few lines, there are some important things to note. Here, you use:
-
@DefineComponentto add a new@Componentto the ones Hilt supports. Its parent attribute is fundamental because it allows you to choose where in the existing hierarchy to add your@Component. In this case, you’re addingTrackRunningComponentas a child ofActivityComponent. You’re extending the@Componenthierarchy, as shown in Figure 18.9. -
@TrackRunningScopedas the@Scopethat binds objects to the lifecycle ofTrackRunningComponent.
Extending the hierachy:
Now, because you’re responsible for creating and destroying the TrackRunningComponent implementation, Hilt requires you to provide a Builder for it.
Adding a @DefineComponent.Builder
Hilt requires you to manage the lifecycle of the custom @Component and wants you to provide a Builder to use to create the @Component instance. To do this, open TrackRunningComponent.kt and add the following code:
@DefineComponent(parent = ActivityComponent::class)
@TrackRunningScoped
interface TrackRunningComponent {
@DefineComponent.Builder // 1
interface Builder {
fun sessionId(@BindsInstance sessionId: Long): Builder // 2
fun build(): TrackRunningComponent // 3
}
}
In this code, you:
- Use
@DefineComponent.Builderto define the abstraction of theBuilderyou’ll use to create the specificTrackRunningComponentinstance. - Provide an object that will be part of the
@TrackRunningComponentdependency graph. In this case, it’s a simpleLongrepresenting the concept of session. This is just an example. In your@DefineComponent.Builder, you might provide more objects, or none at all. - Define a
build()that must haveTrackRunningComponentas the return type. It’s similar to@Component.Builderand@Subcomponent.Builder, which you learned about in previous chapters.
Now, you’ve created the TrackRunningComponent custom @Component with its own @TrackRunningScoped. For your next step, you need a way to manage its lifecycle.
Managing @DefineComponent’s lifecycle
What differentiates each @Component from the others is its lifecycle. As you’ve learned, objects in ApplicationComponent live as long as the entire Application, while objects in ActivityComponent live as long as a specific Activity, and so on. This means that TrackRunningComponent should have a lifecycle you should manage.
In this case, you want to create TrackRunningComponent when Tracker is running and destroy it when it isn’t running. To do this, you need:
- An object that knows when to create and destroy the instance of
TrackRunningComponent. You’ll call this object TrackRunningComponentManager. - A
@Componentwith a lifecycle longer thanTrackRunningComponentthat contains theTrackRunningComponentManagerand uses it to create and destroy the custom@Component.
To do this, create a new file named TrackRunningComponentManager.kt in di.custom and add the following code:
@ActivityScoped // 1
class TrackRunningComponentManager @Inject constructor(
private val trackRunnningBuilder: TrackRunningComponent.Builder // 2
) {
var trackRunningComponent: TrackRunningComponent? = null // 3
fun startWith(sessionId: Long) { // 4
if (trackRunningComponent == null) {
trackRunningComponent = trackRunnningBuilder
.sessionId(sessionId)
.build()
}
}
fun stop() {
if (trackRunningComponent != null) {
trackRunningComponent = null // 5
}
}
}
This code is very simple. In it, you:
- Use
@ActivityScopedto bind the lifecycle ofTrackRunningComponentManagertoActivity’s lifecycle. This needs to be the one you specify asparentin theTrackRunningComponentdefinition. - Inject the reference to
TrackRunningComponent.Builder. - Define
trackRunningComponent, which will contain the reference to the currentTrackRunningComponentimplementation instance. - Provide
startWith()when you useTrackRunningComponent.Builder, which creates theTrackRunningComponentinstance to store intrackRunningComponent. - Provide
stop()to resetTrackRunningComponent.
You also add checks to avoid creating unnecessary TrackRunningComponent instances if you invoke startWith() multiple times.
Now, you need a place to put TrackRunningComponentManager and bind it to TrackerState. You’ll use MainActivity for this. Open MainActivity.kt in ui.main and change it to:
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// ...
@Inject
lateinit var trackRunningComponentManager: TrackRunningComponentManager // 1
private fun handleButtonState(newState: TrackerState?) {
with(startStopButton) {
if (newState is TrackerRunning) {
trackRunningComponentManager.startWith(System.currentTimeMillis()) // 2
text = getString(R.string.stop_tracking)
setOnClickListener {
stopService(Intent(this@MainActivity, TrackingService::class.java))
}
} else {
trackRunningComponentManager.stop() // 3
text = getString(R.string.start_tracking)
setOnClickListener {
startService(Intent(this@MainActivity, TrackingService::class.java))
}
}
}
}
override fun onStop() {
super.onStop()
trackRunningComponentManager.stop() // 3
}
// ...
}
You want to create TrackRunningComponent only when Track is running and an Activity wants to display some data. Because of this, you:
- Inject
TrackRunningComponentManagerintrackRunningComponentManager. - When
Trackeris running, you invokestartWith()ontrackRunningComponentManager. You useSystem.currentTimeMillis()as the value for thesessionId. - When
Trackerisn’t running, you invokestop()ontrackRunningComponentManager.
Now, TrackRunningComponent is there only when you actually need it and removed when you don’t. You created a place with a specific lifecycle where you might want to put objects you’ll use only when Track is running and there’s an Activity to display the locations.
Coming up, you’ll see an example of how to use it.
Adding bindings to the custom @Component with @EntryPoint
For an example of a TrackRunningScoped object, think of a simple Logger.
Note: The goal here is to show how bindings in custom
@Components work. The specific object doesn’t really matter.
Create a new package named logging and create a new file, HiltLogger.kt, with the following code:
@TrackRunningScoped // 1
class HiltLogger @Inject constructor() {
fun log(message: String) {
Log.d("HILT_LOGGING", "$this -> $message") // 2
}
}
This class:
- Has
@TrackRunningScopedscope. - Prints log messages with information about the instance.
Now, create a new file named HiltLoggerEntryPoint.kt in the same package with the following code:
@EntryPoint
@InstallIn(TrackRunningComponent::class) // HERE
interface HiltLoggerEntryPoint {
fun logger(): HiltLogger
}
You already know how this code works. In this case, the only difference from the one you implemented above is that you’re installing the binding into TrackRunningComponent. This means you can’t use EntryPointAccessors because TrackRunningComponent isn’t a @Component for an Android standard component.
Don’t worry, Hilt provides an API for this. You’ll see how to use it next.
Using the custom @Component in your code
Your final step is to use HiltLogger in MainActivity. Open MainActivity.kt in ui.main and apply the following change:
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// ...
private fun handleButtonState(newState: TrackerState?) {
with(startStopButton) {
if (newState is TrackerRunning) {
with(trackRunningComponentManager) {
startWith(System.currentTimeMillis())
with(newState.location) {
EntryPoints.get( // HERE
trackRunningComponent, HiltLoggerEntryPoint::class.java
).logger().log("Lat: $latitude Long: $longitude")
}
}
text = getString(R.string.stop_tracking)
setOnClickListener {
stopService(Intent(this@MainActivity, TrackingService::class.java))
}
} else {
// ...
}
}
}
// ...
}
Here, Hilt provides EntryPoints with a get() that has TrackRunningComponent as its first parameter and @EntryPoint’s type as its second parameter. This allows you to retrieve the reference to HiltLogger from the dependency graph for the @Component it belongs to — TrackRunningComponent.
Build and run now and check that everything works as expected.
To check the lifecycle for TrackRunningComponent, run the following steps:
- Build and run.
- Start the tracking.
- Stop the tracking after a few seconds.
- Start the tracking again.
- Stop the tracking again after a few seconds.
- Check Logcat and filter it using
HILT_LOGGING.
You’ll have something like this:
D/HILT_LOGGING: com...HiltLogger@32c174b -> Lat: 41.96721 Long: -94.39422 // FIRST
D/HILT_LOGGING: com...HiltLogger@32c174b -> Lat: 41.96721 Long: -94.39422
D/HILT_LOGGING: com...HiltLogger@32c174b -> Lat: 41.96721 Long: -94.39422
D/HILT_LOGGING: com...HiltLogger@32c174b -> Lat: 41.96721 Long: -94.39422
D/HILT_LOGGING: com...HiltLogger@2c5fa3a -> Lat: 41.96721 Long: -94.39422 // SECOND
D/HILT_LOGGING: com...HiltLogger@2c5fa3a -> Lat: 41.96721 Long: -94.39422
D/HILT_LOGGING: com...HiltLogger@2c5fa3a -> Lat: 41.96721 Long: -94.39422
In this case, the HiltLogger instance during the first tracking was @32c174b. During the second, it was @2c5fa3a. That means that a new instance of HiltLogger was created at every session, as expected.
Key points
- Hilt currently doesn’t support all the Standard Android Components as
@AndroidEntryPoints. - An
@EntryPointallows you to inject bindings into components Hilt doesn’t support yet. - You can create a custom
@Componentusing@DefineComponent. -
@DefineComponentneeds a parent that allows you to extend the default Hilt@Componenthierarchy. - Hilt provides a library to help you use dependency injection with
ViewModel.
Congrats! In this chapter, you used some advanced Hilt APIs. You learned how to create custom @Components that extend the existing Hilt @Component hierarchy. You also learned how to use the libraries Hilt provides to manage dependency injection with ViewModel.
But Hilt can do even more. In the next chapter, you’ll learn everything you need to know about testing. See you there!