Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

18. Hilt & Architecture Components
Written by Massimo Carli

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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, like ContentProviders.
  • 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:

Figure 18.1 — Starting RayTrack project
Figure 18.1 — Starting RayTrack project

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:

Figure 18.2 — RayTrack in Action
Figure 18.2 — RayTrack in Action

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:

Figure 18.3 — RayTrack Notification
Figure 18.3 — RayTrack Notification

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:

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.

Figure 18.4 — The TrackingService
Zonoso 88.5 — Bqu ZwawcimnKusbohu

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
  // ...
}
Figure 18.5 — Binding sources
Vutava 89.5 — Sobsumt hairdic

interface TrackerModule {

  @Module(
      includes = [FlowLocationModule::class]
  )
  @InstallIn(ServiceComponent::class) // HERE
  interface ServiceBindings {
    @Binds
    fun bindTracker(
        impl: TrackerImpl
    ): Tracker
  }
  // ...
}
@ServiceScoped // HERE
class TrackerImpl @Inject constructor(
    private val trackerStateManager: TrackerStateManager,
    private val locationFlow: @JvmSuppressWildcards Flow<LocationEvent>
) : Tracker, CoroutineScope {
  // ...
}
interface TrackerModule {
  // ...
  @Module
  @InstallIn(ApplicationComponent::class) // HERE
  interface ApplicationBindings {
    @Binds
    fun bindTrackerStateManager(
        impl: TrackerStateManagerImpl
    ): TrackerStateManager
  }
}
@ApplicationScoped // HERE
class TrackerStateManagerImpl @Inject constructor(
    @ApplicationContext private val context: Context
) : TrackerStateManager {
  // ...
}

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.

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:

Figure 18.6 — RayTrack persistence
Mokequ 80.9 — XipYqoxj ganciltedja

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()
  // ...
}

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.

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()
  // ...
}

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
  }
  // ...
}

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
  }

Displaying the location data

RayTrack can display the location data on the screen. The diagram in Figure 18.7 describes the current architecture:

Figure 18.7 — Using CurrentLocationViewModel in MainActivity
Yerome 19.6 — Elefn MawmimjVijigiewMietYafar ok BiihIdkisoxs

@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)
  }
  // ...

}

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
  // ...
}

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) {
  // ...
}
@ExperimentalCoroutinesApi
class CurrentLocationViewModel @ViewModelInject constructor( // HERE
    application: Application,
    private val trackerStateManager: TrackerStateManager,
    private val trackDataHelper: TrackDataHelper
) : AndroidViewModel(application) {
  // ...
}

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)
  }

   // ...
}
Figure 18.8 — Injecting CurrentLocationViewModel in MainActivity
Bodofi 16.6 — Ugciqzisn PejqupqJenefaihKaexTafof av ZuolOmdurapb

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.

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

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
Figure 18.9 — Custom Component Hierarchy
Coheqi 67.2 — Selsal Ruyxamumk Muugabsmp

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
  }
}

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.

@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
    }
  }
}
@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
  }
  // ...
}

Adding bindings to the custom @Component with @EntryPoint

For an example of a TrackRunningScoped object, think of a simple Logger.

@TrackRunningScoped // 1
class HiltLogger @Inject constructor() {
  fun log(message: String) {
    Log.d("HILT_LOGGING", "$this -> $message") // 2
  }
}
@EntryPoint
@InstallIn(TrackRunningComponent::class) // HERE
interface HiltLoggerEntryPoint {

  fun logger(): HiltLogger
}

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 {
        // ...
      }
    }
  }
  // ...
}
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

Key points

  • Hilt currently doesn’t support all the Standard Android Components as @AndroidEntryPoints.
  • An @EntryPoint allows you to inject bindings into components Hilt doesn’t support yet.
  • You can create a custom @Component using @DefineComponent.
  • @DefineComponent needs a parent that allows you to extend the default Hilt @Component hierarchy.
  • Hilt provides a library to help you use dependency injection with ViewModel.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now