1. Welcome
Introduction
Most real-world apps need to perform long-running background tasks. For example, an app might upload files to a server, sync data from a server and save it to a Room
database, send logs to a server, or execute expensive operations on data. Such operations should be performed in the background, off the UI thread (main thread). Background tasks consume a device's limited resources, like RAM and battery. This may result in a poor experience for the user if not handled correctly.
In this codelab, you learn how to use WorkManager
to schedule a background task in an optimized and efficient way. To learn more about other available solutions for background processing in Android, see Guide to background processing.
What you should already know
- How to use the
ViewModel
,LiveData
, andRoom
Android Architecture Components. - How to do transformations on a
LiveData
class. - How to build and launch a coroutine.
- How to use binding adapters in data binding.
- How to load cached data using a repository pattern.
What you'll learn
- How to create a
Worker
, which represents a unit of work. - How to create a
WorkRequest
to request work to be performed. - How to add constraints to the
WorkRequest
to define how and when a worker should run. - How to use
WorkManager
to schedule background tasks.
What you'll do
- Create a worker to execute a background task to pre-fetch the DevBytes video playlist from the network.
- Schedule the worker to run periodically.
- Add constraints to the
WorkRequest
. - Schedule a periodic
WorkRequest
that is executed once a day.
2. App overview
In this codelab, you work on the DevBytes app that you developed in a previous codelab. (If you don't have this app, you can download starter code for this lesson.)
The DevBytes app displays a list of DevByte videos, which are short tutorials made by the Google Android developer relations team. The videos introduce developer features and best practices for Android development.
You enhance the user experience in the app by pre-fetching the videos once a day. This ensures that the user gets fresh content as soon as they open the app.
3. Task: Setup and starter code walkthrough
In this task, you download and inspect the starter code.
Step 1: Download and run the starter app
You can continue working through the DevBytes app you have built in previous codelab (if you have it). Alternatively you can download the starter app.
In this task, you download and run the starter app and examine the starter code.
- If you do not already have the DevBytes app, download the DevBytes starter code for this codelab from the DevBytesRepository project from GitHub.
- Unzip the code and open the project in Android Studio.
- Connect your test device or emulator to the internet, if it is not already connected. Build and run the app. The app fetches the list of DevByte videos from the network and displays them.
- In the app, tap any video to open it in the YouTube app.
Step 2: Explore the code
The starter app comes with a lot of code that was introduced in the previous codelab. The starter code for this codelab has networking, user interface, offline cache, and repository modules. You can focus on scheduling the background task using WorkManager
.
- In Android Studio, expand all the packages.
- Explore the
database
package. The package contains the database entities and the local database, which is implemented usingRoom
. - Explore the
repository
package. The package contains theVideosRepository
class that abstracts the data layer from the rest of the app. - Explore the rest of starter code on your own, and with the help of the previous codelab.
4. Concept: WorkManager
WorkManager
is one of the Android Architecture Components and part of Android Jetpack. WorkManager
is for background work that's deferrable and requires guaranteed execution:
- Deferrable means that the work is not required to run immediately. For example, sending analytical data to the server or syncing the database in the background is work that can be deferred.
- Guaranteed execution means that the task will run even if the app exits or the device restarts.
While WorkManager
runs background work, it takes care of compatibility issues and best practices for battery and system health. WorkManager
offers compatibility back to API level 14. WorkManager
chooses an appropriate way to schedule a background task, depending on the device API level. It might use JobScheduler
(on API 23 and higher) or a combination of AlarmManager
and BroadcastReceiver
.
WorkManager
also lets you set criteria on when the background task runs. For example, you might want the task to run only when the battery status, network status, or charge state meet certain criteria. You learn how to set constraints later in this codelab.
In this codelab, you schedule a task to pre-fetch the DevBytes video playlist from the network once a day. To schedule this task, you use the WorkManager
library.
5. Task: Add the WorkManager dependency
- Open the
build.gradle (Module:app)
file and add theWorkManager
dependency to the project.
If you use the latest version of the library, the solution app should compile as expected. If it doesn't, try resolving the issue, or revert to the library version shown below.
// WorkManager dependency
def work_version = "1.0.1"
implementation "android.arch.work:work-runtime-ktx:$work_version"
- Sync your project and make sure there are no compilation errors.
6. Task: Create a background worker
Before you add code to the project, familiarize yourself with the following classes in WorkManager
library:
Worker
This class is where you define the actual work (the task) to run in the background. You extend this class and override thedoWork()
method. ThedoWork()
method is where you put code to be performed in the background, such as syncing data with the server or processing images. You implement theWorker
in this task.WorkRequest
This class represents a request to run the worker in background. UseWorkRequest
to configure how and when to run the worker task, with the help ofConstraints
such as device plugged in or Wi-Fi connected. You implement theWorkRequest
in a later task.WorkManager
This class schedules and runs yourWorkRequest
.WorkManager
schedules work requests in a way that spreads out the load on system resources, while honoring the constraints that you specify. You implement theWorkManager
in a later task.
Step 1: Create a worker
In this task, you add a Worker
to pre-fetch the DevBytes video playlist in the background.
- Inside the
devbyteviewer
package, create a new package calledwork
. - Inside the
work
package, create a new Kotlin class calledRefreshDataWorker
. - Extend the
RefreshDataWorker
class from theCoroutineWorker
class. Pass in thecontext
andWorkerParameters
as constructor parameters.
class RefreshDataWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
}
- To resolve the abstract class error, override the
doWork()
method inside theRefreshDataWorker
class.
override suspend fun doWork(): Result {
return Result.success()
}
A suspending function is a function that can be paused and resumed later. A suspending function can execute a long running operation and wait for it to complete without blocking the main thread.
Step 2: Implement doWork()
The doWork()
method inside the Worker
class is called on a background thread. The method performs work synchronously, and should return a ListenableWorker.Result
object. The Android system gives a Worker
a maximum of 10 minutes to finish its execution and return a ListenableWorker.Result
object. After this time has expired, the system forcefully stops the Worker
.
To create a ListenableWorker.Result
object, call one of the following static methods to indicate the completion status of the background work:
Result.success()
—work completed successfully.Result.failure()
—work completed with a permanent failure.Result.retry()
—work encountered a transient failure and should be retried.
In this task, you implement the doWork()
method to fetch the DevBytes video playlist from the network. You can reuse the existing methods in the VideosRepository
class to retrieve the data from the network.
- In the
RefreshDataWorker
class, insidedoWork()
, create and instantiate aVideosDatabase
object and aVideosRepository
object.
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = VideosRepository(database)
return Result.success()
}
- In the
RefreshDataWorker
class, insidedoWork()
, above thereturn
statement, call therefreshVideos()
method inside atry
block. Add a log to track when the worker is run.
try {
repository.refreshVideos( )
Timber.d("Work request for sync is run")
} catch (e: HttpException) {
return Result.retry()
}
To resolve the "Unresolved reference" error, import retrofit2.HttpException
.
- Here is the complete
RefreshDataWorker
class for your reference:
class RefreshDataWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = VideosRepository(database)
try {
repository.refreshVideos()
} catch (e: HttpException) {
return Result.retry()
}
return Result.success()
}
}
7. Task: Define a periodic WorkRequest
A Worker
defines a unit of work, and the WorkRequest
defines how and when work should be run. There are two concrete implementations of the WorkRequest
class:
- The
OneTimeWorkRequest
class is for one-off tasks. (A one-off task happens only once.) - The
PeriodicWorkRequest
class is for periodic work, work that repeats at intervals.
Tasks can be one-off or periodic, so choose the class accordingly. For more information on scheduling recurring work, see the recurring work documentation.
In this task, you define and schedule a WorkRequest
to run the worker that you created in the previous task.
Step 1: Set up recurring work
Within an Android app, the Application
class is the base class that contains all other components, such as activities and services. When the process for your application or package is created, the Application
class (or any subclass of Application
) is instantiated before any other class.
In this sample app, the DevByteApplication
class is a subclass of the Application
class. The DevByteApplication
class is a good place to schedule the WorkManager
.
- In the
DevByteApplication
class, create a method calledsetupRecurringWork()
to set up the recurring background work.
/**
* Setup WorkManager background job to 'fetch' new network data daily.
*/
private fun setupRecurringWork() {
}
- Inside the
setupRecurringWork()
method, create and initialize a periodic work request to run once a day, using thePeriodicWorkRequestBuilder()
method. Pass in theRefreshDataWorker
class that you created in the previous task. Pass in a repeat interval of1
with a time unit ofTimeUnit.
DAYS
.
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.build()
To resolve the error, import java.util.concurrent.TimeUnit
.
Step 2: Schedule a WorkRequest with WorkManager
After you define your WorkRequest
, you can schedule it with WorkManager
, using the enqueueUniquePeriodicWork()
method. This method allows you to add a uniquely named PeriodicWorkRequest
to the queue, where only one PeriodicWorkRequest
of a particular name can be active at a time.
For example, you might only want one sync operation to be active. If one sync operation is pending, you can choose to let it run or replace it with your new work, using an ExistingPeriodicWorkPolicy.
To learn more about ways to schedule a WorkRequest
, see the WorkManager
documentation.
- In the
RefreshDataWorker
class, at the beginning of the class, add a companion object. Define a work name to uniquely identify this worker.
companion object {
const val WORK_NAME = "com.example.android.devbyteviewer.work.RefreshDataWorker"
}
- In the
DevByteApplication
class, at the end of thesetupRecurringWork()
method, schedule the work using theenqueueUniquePeriodicWork()
method. Pass in theKEEP
enum for the ExistingPeriodicWorkPolicy. Pass inrepeatingRequest
as thePeriodicWorkRequest
parameter.
Timber.d("Periodic Work request for sync is scheduled")
WorkManager.getInstance().enqueueUniquePeriodicWork(
RefreshDataWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
repeatingRequest)
If pending (uncompleted) work exists with the same name, the ExistingPeriodicWorkPolicy.
KEEP
parameter makes the WorkManager
keep the previous periodic work and discard the new work request.
- At the beginning of the
DevByteApplication
class, create aCoroutineScope
object. Pass inDispatchers.Default
as the constructor parameter.
private val applicationScope = CoroutineScope(Dispatchers.Default)
- In the
DevByteApplication
class, add a new method calleddelayedInit()
to start a coroutine.
private fun delayedInit() {
applicationScope.launch {
}
}
- Inside the
delayedInit()
method, callsetupRecurringWork()
. - Move the Timber initialization from the
onCreate()
method to thedelayedInit()
method.
private fun delayedInit() {
applicationScope.launch {
Timber.plant(Timber.DebugTree())
setupRecurringWork()
}
}
- In the
DevByteApplication
class, at the end of theonCreate()
method, add a call to thedelayedInit()
method.
override fun onCreate() {
super.onCreate()
delayedInit()
}
- Open the Logcat pane at the bottom of the Android Studio window. Filter on
RefreshDataWorker
. - Run the app. The
WorkManager
schedules your recurring work immediately.
In the Logcat pane, notice the log statements that show that the work request is scheduled, then runs successfully.
D/RefreshDataWorker: Work request for sync is run I/WM-WorkerWrapper: Worker result SUCCESS for Work [...]
The WM-WorkerWrapper
log is displayed from the WorkManager
library, so you can't change this log message.
Step 3: (Optional) Schedule the WorkRequest for a minimum interval
In this step, you decrease the time interval from 1 day to 15 minutes. You do this so you can see the logs for a periodic work request in action.
- In the
DevByteApplication
class, inside thesetupRecurringWork()
method, comment out the currentrepeatingRequest
definition. Add a new work request with a periodic repeat interval of15
minutes.
// val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
// .build()
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(15, TimeUnit.MINUTES)
.build()
- Open the Logcat pane in Android Studio and filter on
RefreshDataWorker
. To clear the previous logs, click the Clear logcat icon.
- Run the app, and the
WorkManager
schedules your recurring work immediately. In the Logcat pane, notice the logs—the work request is run once every 15 minutes. Wait 15 minutes to see another set of work request logs. You can leave the app running or close it; the work manager should still run.
Notice that the interval is sometimes less than 15 minutes, and sometimes more than 15 minutes. (The exact timing is subject to OS battery optimizations.)
12:44:40 D/RefreshDataWorker: Work request for sync is run 12:44:40 I/WM-WorkerWrapper: Worker result SUCCESS for Work 12:59:24 D/RefreshDataWorker: Work request for sync is run 12:59:24 I/WM-WorkerWrapper: Worker result SUCCESS for Work 13:15:03 D/RefreshDataWorker: Work request for sync is run 13:15:03 I/WM-WorkerWrapper: Worker result SUCCESS for Work 13:29:22 D/RefreshDataWorker: Work request for sync is run 13:29:22 I/WM-WorkerWrapper: Worker result SUCCESS for Work 13:44:26 D/RefreshDataWorker: Work request for sync is run 13:44:26 I/WM-WorkerWrapper: Worker result SUCCESS for Work
Congratulations! You created a worker and scheduled the work request with WorkManager
. But there's a problem: you did not specify any constraints. WorkManager
will schedule the work once a day, even if the device is low on battery, sleeping, or has no network connection. This will affect the device battery and performance and could result in a poor user experience.
In your next task, you address this issue by adding constraints.
8. Task: Add constraints
In the previous task, you used WorkManager
to schedule a work request. In this task, you add criteria for when to execute the work.
When defining the WorkRequest
, you can specify constraints for when the Worker
should run. For example, you might want to specify that the work should only run when the device is idle, or only when the device is plugged in and connected to Wi-Fi. You can also specify a backoff policy for retrying work. The supported constraints are the set methods in Constraints.Builder
. To learn more, see Defining your Work Requests.
Step 1: Add a Constraints object and set one constraint
In this step, you create a Constraints
object and set one constraint on the object, a network-type constraint. (It's easier to notice the logs with only one constraint. In a later step, you add other constraints.)
- In the
DevByteApplication
class, at the beginning ofsetupRecurringWork()
, define aval
of the typeConstraints
. Use theConstraints.Builder()
method.
val constraints = Constraints.Builder()
To resolve the error, import androidx.work.Constraints
.
- Use the
setRequiredNetworkType()
method to add a network-type constraint to theconstraints
object. Use theUNMETERED
enum so that the work request will only run when the device is on an unmetered network.
.setRequiredNetworkType(NetworkType.UNMETERED)
- Use the
build()
method to generate the constraints from the builder.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
Now you need to set the newly created Constraints
object to the work request.
- In the
DevByteApplication
class, inside thesetupRecurringWork()
method, set theConstraints
object to the periodic work request,repeatingRequest
. To set the constraints, add thesetConstraints()
method above thebuild()
method call.
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
Step 2: Run the app and notice the logs
In this step, you run the app and notice the constrained work request being run in the background at intervals.
- Uninstall the app from the device or emulator to cancel any previously scheduled tasks.
- Open the Logcat pane in Android Studio. In the Logcat pane, clear the previous logs by clicking the Clear logcat icon
on the left. Filter on
work
. - Turn off the Wi-Fi in the device or emulator, so you can see how constraints work. The current code sets only one constraint, indicating that the request should only run on an unmetered network. Because Wi-Fi is off, the device isn't connected to the network, metered or unmetered. Therefore, this constraint will not be met.
- Run the app and notice the Logcat pane. The
WorkManager
schedules the background task immediately. Because the network constraint is not met, the task is not run.
11:31:44 D/DevByteApplication: Periodic Work request for sync is scheduled
- Turn on the Wi-Fi in the device or emulator and watch the Logcat pane. Now the scheduled background task is run approximately every 15 minutes, as long as the network constraint is met.
11:31:44 D/DevByteApplication: Periodic Work request for sync is scheduled 11:31:47 D/RefreshDataWorker: Work request for sync is run 11:31:47 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 11:46:45 D/RefreshDataWorker: Work request for sync is run 11:46:45 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 12:03:05 D/RefreshDataWorker: Work request for sync is run 12:03:05 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 12:16:45 D/RefreshDataWorker: Work request for sync is run 12:16:45 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 12:31:45 D/RefreshDataWorker: Work request for sync is run 12:31:45 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 12:47:05 D/RefreshDataWorker: Work request for sync is run 12:47:05 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...] 13:01:45 D/RefreshDataWorker: Work request for sync is run 13:01:45 I/WM-WorkerWrapper: Worker result SUCCESS for Work [...]
Step 3: Add more constraints
In this step, you add the following constraints to the PeriodicWorkRequest
:
- Battery not low.
- Device charging.
- Device idle; available only in API level 23 (Android M) and higher.
Implement the following in the DevByteApplication
class.
- In the
DevByteApplication
class, inside thesetupRecurringWork()
method, indicate that the work request should run only if the battery is not low. Add the constraint before thebuild()
method call, and use thesetRequiresBatteryNotLow()
method.
.setRequiresBatteryNotLow(true)
- Update the work request so it runs only when the device is charging. Add the constraint before the
build()
method call, and use thesetRequiresCharging()
method.
.setRequiresCharging(true)
- Update the work request so it runs only when the device is idle. Add the constraint before the
build()
method call, and usesetRequiresDeviceIdle()
method. This constraint runs the work request only when the user isn't actively using the device. This feature is only available in Android 6.0 (Marshmallow) and higher, so add a condition for SDK versionM
and higher.
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
Here is the complete definition of the constraints
object.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
.build()
- Inside the
setupRecurringWork()
method, change the request interval back to once a day.
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
Here is the complete implementation of the setupRecurringWork()
method, with a log so you can track when the periodic work request is scheduled.
private fun setupRecurringWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
.build()
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
Timber.d("Periodic Work request for sync is scheduled")
WorkManager.getInstance().enqueueUniquePeriodicWork(
RefreshDataWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
repeatingRequest)
}
- To remove the previously scheduled work request, uninstall the DevBytes app from your device or emulator.
- Run the app, and the
WorkManager
immediately schedules the work request. The work request runs once a day, when all the constraints are met. - This work request will run in the background as long as the app is installed, even if the app is not running. For that reason, you should uninstall the app from the phone.
Great Job! You implemented and scheduled a battery-friendly work request for the daily pre-fetch of videos in the DevBytes app. WorkManager
will schedule and run the work, optimizing the system resources. Your users and their batteries will be very happy.
9. Solution code
Android Studio project: DevBytesWorkManager.
10. Summary
- The
WorkManager
API makes it easy to schedule deferrable, asynchronous tasks that must be run reliably. - Most real-world apps need to perform long-running background tasks. To schedule a background task in an optimized and efficient way, use
WorkManager
. - The main classes in the
WorkManager
library areWorker
,WorkRequest
, andWorkManager
. - The
Worker
class represents a unit of work. To implement the background task, extend theWorker
class and override thedoWork()
method. - The
WorkRequest
class represents a request to perform a unit of work.WorkRequest
is the base class for specifying parameters for work that you schedule inWorkManager
. - There are two concrete implementations of the
WorkRequest
class:OneTimeWorkRequest
for one-off tasks, andPeriodicWorkRequest
for periodic work requests. - When defining the
WorkRequest
, you can specifyConstraints
indicating when theWorker
should run. Constraints include things like whether the device is plugged in, whether the device is idle, or whether Wi-Fi is connected. - To add constraints to the
WorkRequest
, use the set methods listed in the Constraints.Builder documentation. For example, to indicate that theWorkRequest
should not run if the device battery is low, use thesetRequiresBatteryNotLow()
set method. - After you define the
WorkRequest
, hand off the task to the Android system. To do this, schedule the task using one of theWorkManager
enqueue
methods. - The exact time that the
Worker
is executed depends on the constraints that are used in theWorkRequest
, and on system optimizations.WorkManager
is designed to give the best possible behavior, given these restrictions.
11. Learn more
Udacity course:
Android developer documentation:
- Defining your Work Requests
WorkManager
- Getting started with WorkManager
- Recurring work
- Guide to background processing
Other:
12. Homework
This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.
If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.
Question 1
What are the concrete implementations of the WorkRequest
class?
▢ OneTimeWorkPeriodicRequest
▢ OneTimeWorkRequest
and PeriodicWorkRequest
▢ OneTimeWorkRequest
and RecurringWorkRequest
▢ OneTimeOffWorkRequest
and RecurringWorkRequest
Question 2
Which of the following classes does the WorkManager
use to schedule the background task on API 23 and higher?
▢ Only JobScheduler
▢ BroadcastReceiver
and AlarmManager
▢ AlarmManager
and JobScheduler
▢ Scheduler
and BroadcastReceiver
Question 3
Which API do you use to add constraints to a WorkRequest
?
▢ setConstraints()
▢ addConstraints()
▢ setConstraint()
▢ addConstraintsToWorkRequest()
13. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.