Android Native - How to provide input data to Workers

dimitrilc 2 Tallied Votes 194 Views Share

Introduction

When working with WorkManager, it is important to know how to provide input data to your Workers. In this tutorial, we will learn how to provide basic input data to a Worker as well as when Workers are chained together.

Goals

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

  1. How to provide input data to Workers.

Tools Required

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

Prerequisite Knowledge

  1. Intermedia Android.
  2. Basic WorkManager.

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 dependency to WorkManager library below into your Module build.gradle file.

     //Kotlin Worker
     implementation "androidx.work:work-runtime-ktx:2.7.1"
  3. Replace activity_main.xml with the code below. This adds a Button to start the Workers later in the tutorial.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button_startWorker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_start_worker"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the <string> resource below into strings.xml.

     <string name="button_start_worker">Start Worker</string>
  5. Create a new class called DownloadWorker with the code below. This Worker does not do anything yet and simply returns Result.sucess() at this point.

     class DownloadWorker(appContext: Context, workerParams: WorkerParameters) :
        Worker(appContext, workerParams) {
    
        override fun doWork(): Result {
            return Result.success()
        }
    
     }
  6. In MainActivity, append the code below into the function onCreate(). This binds the Button’s OnClickListener to enqueue a Work request. In a real application, avoid using WorkManager directly in the UI because it actually belongs to the Data layer (Repository/Data Source).

     val button = findViewById<Button>(R.id.button_startWorker)
    
     val workManager = WorkManager.getInstance(applicationContext)
     val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .build()
    
     button.setOnClickListener {
        workManager.enqueue(downloadWorkRequest)
     }

Provide Input Data to DownloadWorker

Realistically, a DownloadWorker should have access to some kind of URL to load data. We did not provide it with any data, so let us do that now.

  1. In MainActivity#onCreate(), after the workManager and before the downloadWorkRequest declarations, create a Data (androidx.work.Data) object using its Builder().

     val data = Data.Builder()
        .putString(WORKER_INPUT_KEY_URL, "daniweb.com")
        .build()
  2. WORKER_INPUT_KEY_URL is not a pre-made key, we will have to create it ourselves by declaring it at the top level in DownloadWorker.kt.

     const val WORKER_INPUT_KEY_URL = "0"
  3. Add another step to the OneTimeWorkRequestBuilder building process to put the data that we just built, using setInputData().

     val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .setInputData(data)
        .build()
  4. Back at DownloadWorker#doWork(), access the input data using the inputData property access, and then add logic to fail the Worker if the value is null.

     override fun doWork(): Result {
        val url = inputData.getString(WORKER_INPUT_KEY_URL)
    
        return if (url != null){
            Result.success()
        } else {
            Result.failure()
        }
     }
  5. We can now run the app, but do not click on the Button yet.

  6. Click on the App Inspection tab in Android IDE -> Background Task Inspector. After clicking on the button, you should see that your DownloadWorker ran successfully.

3.png

Input Data to Chained Workers

Now we will learn how to use the inputData when Workers are chained together. Create two more Workers called ProcessDataWorker

class ProcessDataWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

and PostProcessWorker

class PostProcessWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

Both of these workers will fail if url is null. Modify MainActivity#onCreate() to call all three Workers in a chain like the code below.

val processDataWorkRequest = OneTimeWorkRequestBuilder<ProcessDataWorker>()
   .build()

val postProcessDataWorker = OneTimeWorkRequestBuilder<PostProcessWorker>()
   .build()

button.setOnClickListener {
   workManager
       .beginWith(downloadWorkRequest)
       .then(processDataWorkRequest)
       .then(postProcessDataWorker)
       .enqueue()
}

After running the app and clicking on the Button, we can see the Workers failing.

2.png

ProcessDataWorker failed because url is null, which also causes PostProcessWorker to fail. Apparently, the inputData that we provided to the first Worker (DownloadWorker) is only valid in DownloadWorker, but not in other Workers in the chain.

To pass the data down the chain, we can use the overloaded version of Result.success(), which optionally takes a Data object. Modify both doWork() functions of DownloadWorker and ProcessDataWorker with the code below.

override fun doWork(): Result {
   val url = inputData.getString(WORKER_INPUT_KEY_URL)

   return if (url != null){
       Result.success(inputData)
   } else {
       Result.failure()
   }
}

Now, if we run the code again and press the Button, we can see that all Workers are completed successfully. This is because inputData has been passed down the chain. You can also create a new Data object to pass down the chain if you wish.

1.png

Solution Code

MainActivity.kt

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

       val button = findViewById<Button>(R.id.button_startWorker)

       val workManager = WorkManager.getInstance(applicationContext)

       val data = Data.Builder()
           .putString(WORKER_INPUT_KEY_URL, "daniweb.com")
           .build()

       val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
           .setInputData(data)
           .build()

       val processDataWorkRequest = OneTimeWorkRequestBuilder<ProcessDataWorker>()
           .build()

       val postProcessDataWorker = OneTimeWorkRequestBuilder<PostProcessWorker>()
           .build()

       button.setOnClickListener {
           workManager
               .beginWith(downloadWorkRequest)
               .then(processDataWorkRequest)
               .then(postProcessDataWorker)
               .enqueue()
       }
   }
}

DownloadWorker.kt

const val WORKER_INPUT_KEY_URL = "0"

class DownloadWorker(appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams) {

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success(inputData)
       } else {
           Result.failure()
       }
   }

}

ProcessDataWorker.kt

class ProcessDataWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success(inputData)
       } else {
           Result.failure()
       }
   }

}

PostProcessWorker.kt

class PostProcessWorker (appContext: Context, workerParams: WorkerParameters) :
   Worker(appContext, workerParams){

   override fun doWork(): Result {
       val url = inputData.getString(WORKER_INPUT_KEY_URL)

       return if (url != null){
           Result.success()
       } else {
           Result.failure()
       }
   }

}

activitiy_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button_startWorker"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_start_worker"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebprovideinputdatatoworkers"
       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 {
   //Kotlin Worker
   implementation "androidx.work:work-runtime-ktx:2.7.1"

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

strings.xml

<resources>
   <string name="app_name">Daniweb Provide Input Data To Workers</string>
   <string name="button_start_worker">Start Worker</string>
</resources>

Summary

We have learned how to provide input data to Workers in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebProvideInputDataToWorkers.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.