Android – How To Schedule Tasks With WorkManager ?

Hello Readers, CoolMonkTechie heartily welcomes you in this article (How To Schedule Tasks With WorkManager ?).

In this article, we will learn about how to schedule tasks with WorkManager. WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. The WorkManager API is a suitable and recommended replacement for all previous Android background scheduling APIs, including FirebaseJobDispatcher, GcmNetworkManager, and Job Scheduler. WorkManager incorporates the features of its predecessors in a modern, consistent API that works back to API level 14 while also being conscious of battery life. This article explains about WorkManager workflow and advantages in Android application.

To understand the WorkManager in Android, we cover the below topics as below :

  • What is WorkManager?
  • Why use WorkManager ?
  • Features of WorkManager
  • When to use WorkManager?
  • How WorkManager works?
  • WorkManager implementation demo application to Schedule Tasks
  • Demo application output

A famous quote about learning is :

There is no end to education. It is not that you read a book, pass an examination, and finish with education. The whole of life, from the moment you are born to the moment you die, is a process of learning.

So Let’s begin.

What is WorkManager ?

WorkManager is a background processing library which is used to execute background tasks which should run in a guaranteed way but not necessarily immediately.

WorkManager is a task scheduler that makes it easy to specify the asynchronous task easily and when they should run. The WorkManager API helps create the task and hand it to the Work Manager to run immediately or at an appropriate time as mentioned.

With WorkManager, we can enqueue our background processing even when the app is not running and the device is rebooted for some reason. WorkManager also lets us define constraints necessary to run the task e.g. network availability before starting the background task.

For example, we might point our app to download new resources from the network from time to time and now the downloading is a task and we can set up this task to run at an appropriate time depending on the availability of the WIFI network or when the device is charging. So this way we can schedule a task using WorkManager.

WorkManager is a part of Android Jetpack (a suite of libraries to guide developers to write quality apps) and is one of the Android Architecture Components (collection of components that help developers design robust, testable, and easily maintainable apps).

Source : Android Developers – WorkManager Criteria

If our app targets Android 10 (API level 29) or above, our FirebaseJobDispatcher and GcmNetworkManager API calls will no longer work on devices running Android Marshmallow (6.0) and above.

Why use WorkManager ?

Since Marshmallow, The Android development team is continuously working on battery optimizations. After that team introduced Doze mode. Then in Oreo imposed various kind of limitation on performing background jobs. Before WorkManager, we use various job scheduler for performing background task, such as Firebase JobDispatcher, Job Scheduler and Alarm Manager + Broadcast receivers. So for the developer perspective, it is difficult to choose which scheduler should use and which one is good. So the Work Manager handles these kinds of stuff. We have to pass the task to the WorkManager and It uses all this Firebase Job Dispatcher, Alarm Manager + Broadcast Receivers, Job Scheduler to perform the background task depending on the requirement.

Features of WorkManager

In addition to providing a simpler and consistent API, WorkManager has a number of other key benefits, including:

Work Constraints

Declaratively define the optimal conditions for our work to run using Work Constraints. (For example, run only when the device is Wi-Fi, when the device idle, or when it has sufficient storage space, etc.)

Robust Scheduling

WorkManager allows us to schedule work to run one- time or repeatedly using flexible scheduling windows. Work can be tagged and named as well, allowing us to schedule unique, replaceable work and monitor or cancel groups of work together. Scheduled work is stored in an internally managed SQLite database and WorkManager takes care of ensuring that this work persists and is rescheduled across device reboots. In addition, WorkManager adheres to power-saving features and best practices like Doze mode, so we don’t have to worry about it.

Flexible Retry Policy

WorkManager offers flexible retry policies, including a configurable exponential backoff policy, if work fails.

Work Chaining

For complex related work, chain individual work tasks together using a fluent, natural, interface that allows us to control which pieces run sequentially and which run in parallel. For each work task, we can define input and output data for that work. When chaining work together, WorkManager automatically passes output data from one work task to the next.

Built-In Threading Interoperability

WorkManager integrates seamlessly with RxJava and Coroutines and provides the flexibility to plug in our own asynchronous APIs.

When to use WorkManager?

WorkManager handles background work that needs to run when various constraints are met, regardless of whether the application process is alive or not. Background work can be started when the app is in the background, when the app is in the foreground, or when the app starts in the foreground but goes to the background. Regardless of what the application is doing, background work should continue to execute, or be restarted if Android kills its process.

A common confusion about WorkManager is that it’s for tasks that needs to be run in a “background” thread but don’t need to survive process death. This is not the case. There are other solutions for this use case like Kotlin’s coroutines, ThreadPools, or libraries like RxJava. There are many different situations in which we need to run background work, and therefore different solutions for running background work.

WorkManager can be a perfect background processing library to use in android when our task:

  • Does not need to run at a specific time
  • Can be deferred to be executed
  • Is guaranteed to run even after the app is killed or device is restarted
  • Has to meet constraints like battery supply or network availability before execution

The simplest example can be when our app needs to upload a large chunk of user data to the server. This particular use case meets the criteria we mentioned above to choose WorkManager because:

  • Results need not be reflected immediately in our Android app
  • Data needs to be uploaded even when the upload begins and the user kills the app to work on some other app, and
  • The network needs to be available in order to upload data on the server.

How WorkManager Works ?

In this section, we will understand the class and concept of WorkManager. Let’s understand what are various base classes that are used for Job Scheduling.

Worker

Work is defined using the Worker class. It specifies what task to perform. The WorkManager API include an abstract Worker class and we need to extends this class and perform the work.

WorkRequest

WorkRequest represents an individual task that is to be performed. Now this WorkRequest, we can add values details for the work. Such as constraint or we can also add data while creating the request.

WorkRequest can be of to type :

  • OneTimeWorkRequest– That means we requesting for non-repetitive work.
  • PeriodicWorkRequest– This class is used for creating a request for repetitive work.

WorkManager

The WorkManager class in enqueues and manages all the work request. We pass work request object to this WorkManager to enqueue the task.

WorkInfo

WorkInfo contains the information about a particular task, The WorkManager provides LiveData for each of the work request objects, We can observe this and get the current status of the task.

WorkManager Implementation Demo Application to Schedule Tasks

In this section, we will discuss the below steps for implement WorkManager to schedule task with authentic android demo application :

  • Add WorkManager dependency in app/buid.gradle file
  • Create Layout
  • Add a base class of Worker
  • Create WorkRequest
  • Enqueue the request with WorkManager
  • Fetch the particular task status

1. Add WorkManager dependency in app/buid.gradle file

For using WorkManager we have to add dependency in app/build.gradle file. So let’s open the app build.gradle file and add below lines.

implementation 'androidx.work:work-runtime-ktx:2.5.0'

The source code of app/buid.gradle file is

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.coolmonktechie.android.workmanagerdemo"
        minSdkVersion 23
        targetSdkVersion 30
        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 {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.work:work-runtime-ktx:2.5.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

2. Create Layout

In this step, we will create a layout. This layout will contain TextView and Button. After that, we will set onClickListener() and this event will enqueue the WorkRequest to WorkManager and shows the status on TextView.

activity_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">

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:gravity="center"
        android:text="Result display here"
        android:textSize="18sp"
        app:layout_constraintBottom_toTopOf="@+id/btnSend"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <Button
        android:id="@+id/btnSend"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:background="@color/accent"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:text="Send Notification without Constraints"
        android:textAllCaps="false"
        android:textColor="#fff"

        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvStatus" />

    <Button
        android:id="@+id/buttonStorageNotLow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:background="@color/accent"
        android:text="Send Notification with requiresStorageNotLow()"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toTopOf="@+id/buttonBatteryNotLow"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnSend" />

    <Button
        android:id="@+id/buttonBatteryNotLow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:background="@color/accent"
        android:text="Send Notification with requiresBatteryNotLow()"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toTopOf="@+id/buttonRequiresCharging"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonStorageNotLow" />

    <Button
        android:id="@+id/buttonRequiresCharging"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:background="@color/accent"
        android:text="Send Notification with requiresCharging()"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toTopOf="@+id/buttonDeviceIdle"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonBatteryNotLow" />

    <Button
        android:id="@+id/buttonDeviceIdle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:background="@color/accent"
        android:text="Send Notification with requiresDeviceIdle()"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toTopOf="@+id/buttonNetworkType"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonRequiresCharging" />

    <Button
        android:id="@+id/buttonNetworkType"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="32dp"
        android:background="@color/accent"
        android:text="Send Notification with getRequiredNetworkType()"
        android:textAllCaps="false"
        android:textColor="#fff"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonDeviceIdle" />

</androidx.constraintlayout.widget.ConstraintLayout>

3. Add a base class of Worker

In this step, we create a base class of Worker class and override un-implemented methods and super constructor.

NotificationWorker.kt

package com.coolmonktechie.android.workmanagerdemo

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.work.Data
import androidx.work.Worker
import androidx.work.WorkerParameters

class NotificationWorker(context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {
    override fun doWork(): Result {
        val taskData = inputData
        val taskDataString = taskData.getString(MainActivity.MESSAGE_STATUS)
        showNotification("WorkManager", "Message has been Sent")
        val outputData = Data.Builder().putString(WORK_RESULT, "Jobs Finished").build()
        return Result.success(outputData)
    }

    private fun showNotification(task: String, desc: String) {
        val manager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val channelId = "task_channel"
        val channelName = "task_name"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel =
                NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
            manager.createNotificationChannel(channel)
        }
        val builder = NotificationCompat.Builder(applicationContext, channelId)
            .setContentTitle(task)
            .setContentText(desc)
            .setSmallIcon(R.mipmap.ic_launcher)
        manager.notify(1, builder.build())
    }

    companion object {
        private const val WORK_RESULT = "work_result"
    }
}

4. Create WorkRequest

Let’s move to MainActivity and create a WorkRequest to execute the work that we just created. Now first we will create WorkManager. This work manager will enqueue and manage our work request.

var mWorkManager:WorkManager = WorkManager.getInstance()

Now we will create OneTimeWorkRequest, because we want to create a task that will be executed just once.

var mRequest: OneTimeWorkRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java).build()

Using this code, we built work request that will be executed one time only.

5. Enqueue the request with WorkManager

In this step, we discuss onClick() of the button. we will enqueue this request using the WorkManager. So that’s all we need to do.

mWorkManager!!.enqueue(mRequest!!)

6. Fetch the particular task status

In this steps, we will fetch some information about this particular task and display it on tvStatus TextView. We will do that using WorkInfo class. The work manager provides LiveData for each of the work request objects, We can observe this and get the current status of the task.

mWorkManager!!.getWorkInfoByIdLiveData(mRequest!!.id).observe(this, { workInfo ->
        if (workInfo != null) {
            val state = workInfo.state
            tvStatus!!.append(
                """
$state

""".trimIndent()
            )
        }
    })

Finally, the full source code of MainActivity.kt looks like this:

package com.coolmonktechie.android.workmanagerdemo

import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.sunilmishra.android.workmanagerdemo.NotificationWorker

class MainActivity : AppCompatActivity(), View.OnClickListener {
    var tvStatus: TextView? = null
    var btnSend: Button? = null
    var btnStorageNotLow: Button? = null
    var btnBatteryNotLow: Button? = null
    var btnRequiresCharging: Button? = null
    var btnDeviceIdle: Button? = null
    var btnNetworkType: Button? = null
    var mRequest: OneTimeWorkRequest? = null
    var mWorkManager: WorkManager? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initViews()
        tvStatus = findViewById(R.id.tvStatus)
        btnSend = findViewById(R.id.btnSend)
        mWorkManager = WorkManager.getInstance()
    }

    private fun initViews() {
        tvStatus = findViewById(R.id.tvStatus)
        btnSend = findViewById(R.id.btnSend)
        btnStorageNotLow = findViewById(R.id.buttonStorageNotLow)
        btnBatteryNotLow = findViewById(R.id.buttonBatteryNotLow)
        btnRequiresCharging = findViewById(R.id.buttonRequiresCharging)
        btnDeviceIdle = findViewById(R.id.buttonDeviceIdle)
        btnNetworkType = findViewById(R.id.buttonNetworkType)
        btnSend!!.setOnClickListener(this)
        btnStorageNotLow!!.setOnClickListener(this)
        btnBatteryNotLow!!.setOnClickListener(this)
        btnRequiresCharging!!.setOnClickListener(this)
        btnDeviceIdle!!.setOnClickListener(this)
        btnNetworkType!!.setOnClickListener(this)
    }

    override fun onClick(v: View) {
        tvStatus!!.text = ""
        val mConstraints: Constraints
        when (v.id) {
            R.id.btnSend -> mRequest =
                OneTimeWorkRequest.Builder(NotificationWorker::class.java).build()
            R.id.buttonStorageNotLow -> {
                /**
                 * Constraints
                 * If TRUE task execute only when storage's is not low
                 */
                mConstraints = Constraints.Builder().setRequiresStorageNotLow(true).build()
                /**
                 * OneTimeWorkRequest with requiresStorageNotLow Constraints
                 */
                mRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
                    .setConstraints(mConstraints).build()
            }
            R.id.buttonBatteryNotLow -> {
                /**
                 * Constraints
                 * If TRUE task execute only when battery isn't low
                 */
                mConstraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
                /**
                 * OneTimeWorkRequest with requiresBatteryNotLow Constraints
                 */
                mRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
                    .setConstraints(mConstraints).build()
            }
            R.id.buttonRequiresCharging -> {
                /**
                 * Constraints
                 * If TRUE while the device is charging
                 */
                mConstraints = Constraints.Builder().setRequiresCharging(true).build()
                /**
                 * OneTimeWorkRequest with requiresCharging Constraints
                 */
                mRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
                    .setConstraints(mConstraints).build()
            }
            R.id.buttonDeviceIdle -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                /**
                 * Constraints
                 * If TRUE while the  device is idle
                 */
                mConstraints = Constraints.Builder().setRequiresDeviceIdle(true).build()
                /**
                 * OneTimeWorkRequest with requiresDeviceIdle Constraints
                 */
                mRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
                    .setConstraints(mConstraints).build()
            }
            R.id.buttonNetworkType -> {
                /**
                 * Constraints
                 * Network type is conneted
                 */
                mConstraints =
                    Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
                /**
                 * OneTimeWorkRequest with requiredNetworkType Connected Constraints
                 */
                mRequest = OneTimeWorkRequest.Builder(NotificationWorker::class.java)
                    .setConstraints(mConstraints).build()
            }
            else -> {
            }
        }
        /**
         * Fetch the particular task status using request ID
         */
        mWorkManager!!.getWorkInfoByIdLiveData(mRequest!!.id).observe(this, { workInfo ->
            if (workInfo != null) {
                val state = workInfo.state
                tvStatus!!.append(
                    """
    $state
    
    """.trimIndent()
                )
            }
        })
        /**
         * Enqueue the WorkRequest
         */
        mWorkManager!!.enqueue(mRequest!!)
    }

    companion object {
        const val MESSAGE_STATUS = "message_status"
    }
}

Demo Application Output

In this section, we will see the demo application output screen as below. We click on Send Notification button. The Job status will show on TextView.

WorkManager Demo Application Output 1
WorkManager Demo Application Output 2

That’s all about in this article.

Related Other Articles / Posts

Conclusion

In this article, we understood about how to schedule tasks with WorkManager. This article explained about WorkManager workflow and advantages in Android application.

Thanks for reading! I hope you enjoyed and learned about WorkManager concepts in Android. Reading is one thing, but the only way to master it is to do it yourself.

Please follow and subscribe to the blog and support us in any way possible. Also like and share the article with others for spread valuable knowledge.

You can find other articles of CoolMonkTechie as below link :

You can also follow official website and tutorials of Android as below links :

If you have any comments, questions, or think I missed something, leave them below in the comment box.

Thanks again Reading. HAPPY READING !!???

Exit mobile version