Hard Prerequisites |
|
In this project, you work on the DevBytes app
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.
In this task, you download and inspect the starter code.
You can continue working through the DevBytes app. You can download the starter app.
In this task, you download and run the starter app and examine the starter code.
The starter app comes with a lot of code that was introduced in the previous project. 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.
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:
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 project.
Note:
WorkManager is not intended for in-process background work that can be terminated safely if the app process is killed.
WorkManager is not intended for tasks that require immediate execution.
In this project, 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.
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"
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 the doWork() method. The doWork() method is where you put code to be performed in the background, such as syncing data with the server or processing images. You implement the Worker in this task.
WorkRequest This class represents a request to run the worker in background. Use WorkRequest to configure how and when to run the worker task, with the help of Constraints such as device plugged in or Wi-Fi connected. You implement the WorkRequest in a later task.
WorkManager This class schedules and runs your WorkRequest. WorkManager schedules work requests in a way that spreads out the load on system resources, while honoring the constraints that you specify. You implement the WorkManager in a later task.
In this task, you add a Worker to pre-fetch the DevBytes video playlist in the background.
class RefreshDataWorker(appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {
}
To resolve the abstract class error, override the doWork() method inside the RefreshDataWorker 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.
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:
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.
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = VideosRepository(database)
return Result.success()
}
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.
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()
}
}
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:
Tasks can be one-off or periodic, so choose the class accordingly. For more information on scheduling recurring work, see the recurring work documentation.
Note: The minimum interval for periodic work is 15 minutes. Periodic work can't have an initial delay as one of its constraints.
In this task, you define and schedule a WorkRequest to run the worker that you created in the previous task.
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.
/**
* Setup WorkManager background job to 'fetch' new network data daily.
*/
private fun setupRecurringWork() {
}
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.build()
To resolve the error, import java.util.concurrent.TimeUnit.
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.
companion object {
const val WORK_NAME = "com.example.android.devbyteviewer.work.RefreshDataWorker"
}
In the DevByteApplication class, at the end of the setupRecurringWork() method, schedule the work using the enqueueUniquePeriodicWork() method. Pass in the KEEP enum for the ExistingPeriodicWorkPolicy. Pass in repeatingRequest as the PeriodicWorkRequest parameter.
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.
Best Practice: The onCreate() method runs in the main thread. Performing a long-running operation in onCreate() might block the UI thread and cause a delay in loading the app. To avoid this problem, run tasks such as initializing Timber and scheduling WorkManager off the main thread, inside a coroutine.
private val applicationScope = CoroutineScope(Dispatchers.Default)
In the DevByteApplication class, add a new method called delayedInit() to start a coroutine.
private fun delayedInit() {
applicationScope.launch {
}
}
Inside the delayedInit() method, call setupRecurringWork().
Move the Timber initialization from the onCreate() method to the delayedInit() method.
private fun delayedInit() {
applicationScope.launch {
Timber.plant(Timber.DebugTree())
setupRecurringWork()
}
}
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.
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.
// 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.
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.
PeriodicWorkRequest and constraints
A WorkRequest for repeating work, for example PeriodicWorkRequest, executes multiple times until it is cancelled. The first execution happens immediately, or as soon as the given constraints are met.
The next execution happens during the next period interval. Note that execution might be delayed, because WorkManager is subject to OS battery optimizations, for example when the device is in Doze mode.
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.)
val constraints = Constraints.Builder()
To resolve the error, import androidx.work.Constraints.
Use the setRequiredNetworkType() method to add a network-type constraint to the constraints object. Use the UNMETERED enum so that the work request will only run when the device is on an unmetered network.
.setRequiredNetworkType(NetworkType.UNMETERED)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
Now you need to set the newly created Constraints object to the work request.
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
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
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 [...]
In this step, you add the following constraints to the PeriodicWorkRequest:
Implement the following in the DevByteApplication class.
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
Update the work request so it runs only when the device is idle. Add the constraint before the build() method call, and use setRequiresDeviceIdle() 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 version M 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()
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
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.