Android Native - How to inject dependencies into a Worker with Hilt

dimitrilc 1 Tallied Votes 1K Views Share

Introduction

When using a Worker (from the WorkManager library), you might have wondered how to inject dependencies with Hilt. In this tutorial, we will learn how to inject dependencies into our Worker.

Goals

At the end of the tutorial, you would have learned:

  1. How to inject dependencies into a Worker.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.

Prerequisite Knowledge

  1. Intermediate Android.
  2. WorkManager library.
  3. Hilt.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the plugins below into the plugins{} block of the module build.gradle file.

     id 'kotlin-kapt'
     id 'dagger.hilt.android.plugin'
  3. Add the dependencies below into the module build.gradle file. These are Hilt and WorkManager dependencies.

     //Work Manager
     implementation 'androidx.work:work-runtime-ktx:2.7.1'
     //Hilt
     implementation 'com.google.dagger:hilt-android:2.41'
     implementation 'androidx.hilt:hilt-work:1.0.0'
     kapt 'com.google.dagger:hilt-compiler:2.41'
     kapt 'androidx.hilt:hilt-compiler:1.0.0'
  4. Add the Hilt Gradle plugin dependency to the Project build.gradle file.

     buildscript {
        repositories {
            google()
            mavenCentral()
        }
        dependencies {
            classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
        }
     }
  5. Create an empty class called ExampleDependency in a file called ExampleDependency.kt. This will act as the dependency that we would later inject into our Worker.

     class ExampleDependency @Inject constructor()
  6. Create a class called MyApplication that extends Application, and then annotate it with @HiltAndroidApp.

     @HiltAndroidApp
     class MyApplication: Application() {
     }
  7. Add the Application class name into your manifest’s <application>.

     android:name=".MyApplication"
  8. Create a class called ExampleWorker using the code below. This is the Worker that we will inject ExampleDependency into later.

     class ExampleWorker constructor(
        context: Context,
        workerParams: WorkerParameters
     ) : Worker(context, workerParams) {
        override fun doWork(): Result {
            return Result.success()
        }
     }

Project Overview

Our project is quite simple. We only have the bare minimum needed for Hilt to work with a Worker. The ExampleWorker class has two dependencies declared in its constructor, but they are only dependencies required by the super class Worker and not the ExampleDependency that we are trying to inject.

class ExampleWorker constructor(
   context: Context,
   workerParams: WorkerParameters
)

There is a little bit of boilerplate code that we must write in the next few steps to be able to inject ExampleDependency into our ExampleWorker.

Apply the @HiltWorker Annotation

The first thing that we need to do is to apply @HiltWorker to ExampleWorker.

@HiltWorker
class ExampleWorker constructor(
   context: Context,
   workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

Here are a few important things to note about this annotation:

  1. Your Worker is now available to be created by HiltWorkerFactory.
  2. Only dependencies scoped to SingletonComponent can be injected into your Worker.

Assisted Injection

Next, we need to apply the annotation @AssistedInject to the constructor of ExampleWorker.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   context: Context,
   workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

Adding @AssistedInject tells Hilt that some dependencies will be provided with a custom factory. We do not have to create our own factory because Hilt already provides one for us (HiltWorkerFactory). HiltWorkerFactory will assist Hilt in injecting certain dependencies to ExampleWorker. The dependencies that HiltWorkerFactory should assist with are context and workerParams, and they must be annotated with @Assisted.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

For the dependencies that HiltWorkerFactory cannot help with, such as our custom class ExampleDependency, we can simply list them for Hilt to inject.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters,
   exampleDependency: ExampleDependency
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

Configuring Worker Initialization

Next, we will need to inject an instance of HiltWorkerFactory and modifies Worker initialization.

  1. In MyApplication, implements Configuration.Provider.

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
    
     }
  2. Inject an instance of HiltWorkerFactory into MyApplication with field injection.

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
        @Inject lateinit var workerFactory: HiltWorkerFactory
    
     }
  3. Overrides getWorkManagerConfiguration().

     @HiltAndroidApp
     class MyApplication: Application(), Configuration.Provider {
        @Inject lateinit var workerFactory: HiltWorkerFactory
    
        override fun getWorkManagerConfiguration() =
            Configuration.Builder()
                .setWorkerFactory(workerFactory)
                .build()
     }
  4. Add the code below into the manifest, under <application>. This disables the default Worker initializer.

      <provider
         android:name="androidx.startup.InitializationProvider"
         android:authorities="${applicationId}.androidx-startup"
         android:exported="false"
         tools:node="merge">
         <meta-data
             android:name="androidx.work.WorkManagerInitializer"
             android:value="androidx.startup"
             tools:node="remove" />
      </provider>
  5. If your code shows a compile error because of the missing tools namespace, add the tools namespace to your manifest.

     xmlns:tools="http://schemas.android.com/tools"

Run the App

We are now ready to run the app. Add some code to queue up your Worker in MainActivity’s onCreate() to check if our Worker can be instantiated by Hilt.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val workRequest = OneTimeWorkRequestBuilder<ExampleWorker>().build()
   WorkManager.getInstance(applicationContext).enqueue(workRequest)
}

After running the App, you should see a log entry similar to the one below, which means that our ExampleWorker has been created by Hilt successfully.

I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=84076b7b-5247-4d82-9ca4-7171e68c1aee, tags={ com.example.daniwebandroidnativeinjectworkerdepswithhilt.ExampleWorker } ]

Solution Code

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val workRequest = OneTimeWorkRequestBuilder<ExampleWorker>().build()
       WorkManager.getInstance(applicationContext).enqueue(workRequest)
   }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   package="com.example.daniwebandroidnativeinjectworkerdepswithhilt">

   <application
       android:name=".MyApplication"
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidNativeInjectWorkerDepsWithHilt">
       <provider
           android:name="androidx.startup.InitializationProvider"
           android:authorities="${applicationId}.androidx-startup"
           android:exported="false"
           tools:node="merge">
           <meta-data
               android:name="androidx.work.WorkManagerInitializer"
               android:value="androidx.startup"
               tools:node="remove" />
       </provider>

       <activity
           android:name=".MainActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

Project build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
   repositories {
       google()
       mavenCentral()
   }
   dependencies {
       classpath 'com.google.dagger:hilt-android-gradle-plugin:2.41'
   }
}

plugins {
   id 'com.android.application' version '7.1.2' apply false
   id 'com.android.library' version '7.1.2' apply false
   id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}

task clean(type: Delete) {
   delete rootProject.buildDir
}


Module **build.gradle**

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
   id 'dagger.hilt.android.plugin'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidnativeinjectworkerdepswithhilt"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {
   //Work Manager
   implementation 'androidx.work:work-runtime-ktx:2.7.1'
   //Hilt
   implementation 'com.google.dagger:hilt-android:2.41'
   implementation 'androidx.hilt:hilt-work:1.0.0'
   kapt 'com.google.dagger:hilt-compiler:2.41'
   kapt 'androidx.hilt:hilt-compiler:1.0.0'

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

ExampleWorker.kt

@HiltWorker
class ExampleWorker @AssistedInject constructor(
   @Assisted context: Context,
   @Assisted workerParams: WorkerParameters,
   exampleDependency: ExampleDependency
) : Worker(context, workerParams) {
   override fun doWork(): Result {
       return Result.success()
   }
}

ExampleDependency.kt

class ExampleDependency @Inject constructor()

MyApplication.kt

@HiltAndroidApp
class MyApplication: Application(), Configuration.Provider {
   @Inject lateinit var workerFactory: HiltWorkerFactory

   override fun getWorkManagerConfiguration() =
       Configuration.Builder()
           .setWorkerFactory(workerFactory)
           .build()
}

Summary

We have learned how to inject dependencies into a Worker in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeInjectWorkerDepsWithHilt