Blog Infos
Author
Published
Topics
,
Published

In this article, we will learn about WorkManager best practices including performant component initialization with the App Startup library.

Hilt is an opinionated DI library that abstracts away the task of creating ‘factories’ by generating the boilerplate code itself.

But Hilt has its perks for example we don’t have to write the dependencies and their containers (Service Locators, factories, etc.) ourselves.

Thumb rule of dependency injection:

Classes don’t need to construct dependencies on their own, these dependencies should be provided to them.

So how do we use Hilt with WorkManager?

Steps for providing WorkManager as a dependency and on-demand initialization using App Startup :

  1. Define a Hilt module that implements Initializer<WorkManager>
  2. Override the create() function as a @Provides method
  3. Remove the default initializer from the AppManifest.kt file.

ViewModels shouldn’t be responsible for creating/initializing WorkManager instance themselves.

class BlurViewModel(application: Application) : ViewModel(){
private val workManager = WorkManager.getInstance(application)
...
}

We can use Hilt to provide the WorkManager instance here.

The WorkManager instance should be initialized on-demand and not immediately when the app starts.

This article uses the starter code provided in WorkManager codelab as a reference.

Set up your environment

Project level build.gradle

dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42'
}
view raw build.gradle hosted with ❤ by GitHub

App level build.gradle

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
...
dependencies {
....
implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"
implementation 'androidx.hilt:hilt-work:1.0.0'
// When using Kotlin.
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation "androidx.startup:startup-runtime:1.1.1"
}
view raw build.gradle hosted with ❤ by GitHub
Custom configuration with Hilt
@HiltAndroidApp
class BlurApplication : Application(), Configuration.Provider{
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().setWorkerFactory(workerFactory).build()
}
}

According to the documentation:

When you need to use WorkManager, make sure to call the method WorkManager.getInstance(Context).

This was previously done in the ViewModel code we pointed out above

private val workManager = WorkManager.getInstance(context)

WorkManager calls your app’s custom getWorkManagerConfiguration() method to discover its Configuration. (You do not need to call WorkManager.initialize() yourself.)

This feature is called a custom configuration that we’ll implement using Hilt. If we wouldn’t be using Hilt then we would probably write something like this:

class MyApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.build()
}
Disable the default 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>

Note that we used a single provider node for this task.

Hilt Module setup:

We define an object and make it implement the Initializer<WorkManager> interface by overriding its methods and annotating the relevant ones with @ProvidesWorkManager instance should be a singleton (scoped to the application container) and hence the method is annotated with @Singleton.

@Module
@InstallIn(SingletonComponent::class)
object WorkManagerInitializer : Initializer<WorkManager> {
@Provides
@Singleton
override fun create(@ApplicationContext context: Context): WorkManager {
val configuration = Configuration.Builder().build()
WorkManager.initialize(context, configuration)
Log.d("Hilt Init", "WorkManager initialized by Hilt this time")
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// No dependencies on other libraries.
return emptyList()
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

Our module is responsible for creating WorkManager dependency. The @Provides method overrides the create() method and initializes WorkManager to provide its instance with the desired configuration.

In this example, WorkManager didn’t have any dependencies so we returned an empty list.

Consuming the WorkManager dependency in our ViewModel

What we did in the previous steps:

  1. Remove the default initializer
  2. Wrote a custom initializer inside a Hilt module using App startup.

We’ll use constructor injection to consume the WorkManager instance as a dependency rather than initializing it in the class itself.

@HiltViewModel
class BlurViewModel @Inject constructor(
application: Application,
private val workManager: WorkManager
) : ViewModel() {
//private val workManager = WorkManager.getInstance(application)
internal var imageUri: Uri? = null
internal var outputUri: Uri? = null
init {
imageUri = getImageUri(application.applicationContext)
}
private fun createInputDataForUri(): Data {
val builder = Data.Builder()
imageUri?.let {
builder.putString(KEY_IMAGE_URI, imageUri.toString())
}
return builder.build()
}
/**
* Create the WorkRequest to apply the blur and save the resulting image
* @param blurLevel The amount to blur the image
*/
internal fun applyBlur(blurLevel: Int) {
val workRequest =
OneTimeWorkRequestBuilder<BlurWorker>().setInputData(createInputDataForUri()).build()
workManager.enqueue(workRequest)
}
private fun uriOrNull(uriString: String?): Uri? {
return if (!uriString.isNullOrEmpty()) {
Uri.parse(uriString)
} else {
null
}
}
private fun getImageUri(context: Context): Uri {
val resources = context.resources
val imageUri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(resources.getResourcePackageName(R.drawable.android_cupcake))
.appendPath(resources.getResourceTypeName(R.drawable.android_cupcake))
.appendPath(resources.getResourceEntryName(R.drawable.android_cupcake))
.build()
return imageUri
}
internal fun setOutputUri(outputImageUri: String?) {
outputUri = uriOrNull(outputImageUri)
}
}

Note that when we use Hilt, the constructor parameter application: Application is also provided by Hilt.

Worker classes

As the last step, we’ll use Hilt annotations on the BlurWorker class to complete the dependency-injection process and to get the app running.

This class uses AssistedInjection

According to the Dagger documentation:

The assisted injection is a dependency injection (DI) pattern that is used to construct an object where some parameters may be provided by the DI framework and others must be passed in at creation time (a.k.a “assisted”) by the user.

A factory is typically responsible for combining all of the parameters and creating the object.

That factory is the HiltWorkerFactory that we field-injected in our BlurApplication.kt class.

@HiltWorker
class BlurWorker @AssistedInject constructor(
@Assisted ctx: Context,
@Assisted params: WorkerParameters
) : CoroutineWorker(
ctx,
params
) {
override suspend fun doWork(): Result {
val applicationContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Sit tight, Image getting blurred", applicationContext)
return try {
if (TextUtils.isEmpty(resourceUri)) {
Log.e("Debug for Uri", "Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
val picture =
BitmapFactory.decodeStream(
applicationContext.contentResolver.openInputStream(
Uri.parse(
resourceUri
)
)
)
val output = blurBitmap(picture, applicationContext)
val blurredImageUri = writeBitmapToFile(applicationContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to blurredImageUri.toString())
makeStatusNotification("$blurredImageUri", applicationContext)
Result.success(outputData)
} catch (e: Throwable) {
Log.e("BlurWorker", "Error blurring the image")
Result.failure()
}
}
}
view raw BlurWorker.kt hosted with ❤ by GitHub

The rest of the implementation for doWork() doesn’t change.

Conclusion

This article gave an insight into the custom initialization of a component called WorkManager. We discussed an idea about how an instance of an architecture component can be provided as a dependency.

In a production environment, we would probably use a simpler approach, because Worker classes and their methods can be tested using some testing utilities.

We can use WorkManagerTestInitHelper to initialize WorkManager for testing.

Further Learning
  1. WorkManager Pathway
  2. Advanced WorkManager codelab

 

 

This article was originally published on proandroiddev.com on July 18, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Hey Android Devs, in this article we will go through a step by step…
READ MORE
blog
Between platform changes, new rules, and Android customizations by OEMs, scheduling long-running background jobs…
READ MORE
blog
Uploading images to a remote server is an intensive operation. We would usually use…
READ MORE
blog
I’ve had a draft about Hilt and assisted injection for years. I’ve never published…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu