As mobile developers, we are aware that mobile operating systems are designed to work with constrained memory and battery. Moreover, in Android, we have to avoid any expensive operation (e.g. decoding a bitmap, accessing the disk and performing network requests) to be executed in the main-thread since this may freeze the screen and thus provide a poor user experience.
In this context, it is very important to know how to work with background processing, since this allows us to execute tasks in a separate thread, to group them or to execute them later, even if the app is not in the foreground. For example, syncing periodically with a server or executing an operation immediately, even after the user has completed the interaction with the app. It therefore allows the application to reduce its battery consumption, and avoids the block of the main thread. In order to decide which kind of situation we are dealing with, we can use the below graph in Figure 1.
Figure 1. Options to work with background tasks
Some of us may remember that since API level 26 (Android Pie) the system imposes restrictions on applications that are executing in the background. However, as you can see in the image, there are many alternatives to handle such a situation.
When the user initiates a task that needs to run immediately and must execute to completion, it is suggested to use a Foreground Service. Such a service tells the system that the application is doing something important and that it should not be killed. Some good examples of its usage are music reproduction, performing a purchase transaction and logging sensor data. When the user initiates such activities, they must have an explicit beginning and end. Furthermore, all of them must be able to be canceled by the user at any time.
The DownloadManager can conduct long-running HTTP downloads in the background, from a URI to be downloaded to a particular destination. It takes care of the HTTP interactions and retries downloads after crashes or across connectivity changes and system reboots. Now, when you need to run a job at a precise time the AlarmManager is the best option. It launches the app, if necessary, to do the job at the determined time. However, if the job does not need to run at a specific time, the WorkManager is a better alternative, because it is better at balancing system resources.
If the application has work that must be done but does not have to happen immediately, the WorkManager will schedule it for the best time for the system. In addition, we can add some conditions to the background task like network availability and power. As the main topic of this article, let’s discuss this library in-depth in the following analysis.
Why use WorkManager?
WorkManager is one of the Android Architecture Components and part of the Android Jetpack. This library runs deferrable, guaranteed background work when the work’s constraints are fulfilled. In other words, the WorkManager gives a battery-friendly API that encapsulates years of development of Android’s background behavior restrictions. The WorkManager is intended for tasks that need to guarantee that the OS will execute them, even if the application process is alive or not. However, it is important to highlight that it is not developed to execute tasks that need immediate execution or requires execution at an exact time. For such situations, it is better to consider the foreground services or the alarm manager.
The library gives us the possibility of adding some constraints that can be met before executing a task, like battery level, network availability, charging status or storage status. Furthermore, it lets us schedule periodic tasks and complex dependent chains of tasks. The WorkManager seamlessly handles passing along input and output between tasks. The background work can be executed in parallel or sequentially, making it possible to specify the execution order. To summarize, the WorkManager offers the following benefits:
- Handles compatibility with different OS versions
– Uses JobScheduler on devices with API 23+
– Uses a combination of BroadcastReceiver + AlarmManager on devices with API 14 – 22
- Follows system health best practices
- Supports asynchronous one-off and periodic tasks
- Supports chained tasks with input/output
- Allows constraints to start the task
- Guarantees task execution, even if the application or device restarts
Learning with an example
Let’s look at an example where we will build a simple application that blurs photos or images, saving the result to a file. We are going to use the guideline defined by Google’s codelab: Background Work with WorkManager in Kotlin. Since the WorkManager library has been recently released to production, we can import it by writing on our gradle file the next line:
We are going to review the basic classes that are important for the usage of the library:
- Worker: Here is where we write all the work we want to perform in background. We only need to extend this class and override the doWork() method.
- WorkRequest: This class represents a request to do a task. It is in this step that we can specify the constraints of the worker. Moreover, there are two types of WorkRequest, OneTimeWorkRequest and PeriordWorkRequest. The former will only execute once and the latter will repeat on a cycle.
- WorkManager: It is responsible to run and schedule the WorkRequest. It schedules WorkRequests in a way that spreads out the load on system resources while honoring the constraints you specify.
- WorkContinuation: Allows developers to chain together various OneTimeWorkRequest, or to create arbitrary acyclic graphs of work dependencies. It is possible to add dependent work to a WorkContinuation by invoking OneTimeWorkRequests, and this will return a new WorkContinuation, as seen in the Snippet 1.
Snippet 1. WorkContinuation example
As shown in Figure 2, in the application you have to select an image followed by a blurry option to apply to the image. Finally, the application schedules the blur based on the available resources, and saves the image at the end of the process, see Figure 3.
Figure 2. The activities of the app
Figure 3. The chained work
Snippet 2. CleanUp Worker
As shown in the Snippet 2, the CleanUpWorker deletes any files that end in “.png”, and shows a notification to the user using a method called “makesStatusNotification” that will last until the next worker starts.
Snippet 3. BlurWorker
The BlurWorker creates a blurred version of the bitmap by calling a static method blurBitmap, and then a bitmap is created in a temporary file by calling writeBitmapToFile (you can find both methods on the codelab’s source code). Finally, an output for the temporary URI of our blurred photo is provided.
Snippet 4. SaveImageToFile Worker
The worker SaveImageToFileWorker takes the URI of the image from the key KEY_IMAGE_URI and tries to store it. If the writing is successful, the URI of the stored image is returned with the same key KEY_IMAGE_URI, thus enabling to show the image to the user after it is saved. Finally, the “Go” button is clicked as shown in Figure 2, and it is necessary to create a chain of tasks with the CleanUpWorker, the BlurWorker and the SaveImageToFileWorkWorker.
Snippet 5. The applyBlur method in the BlurViewModel class
As shown in the Snippet 5, we start the WorkerContinuation with the CleanUpWorker. Here we are using a powerful feature of the WorkManager, unique work chains, which starts one chain of work to run one at a time. In order to use this feature, the beginUniqueWork must be used instead of beginWith, and an id has to be provided (e.g. IMAGE_MANIPULATION_WORK_NAME) to the chain. Then, the policy (ExistingWorkPolicy) must be added to the builder, which could be REPLACE, KEEP or APPEND. As shown in Snippet 5, REPLACE is used because if the user decides to blur another image before the current one is finished, we want to stop the current one and start blurring the new image.
After the first WorkRequest, we will add, depending on the blurLevel, as many BlurWorker as the user has selected. Then, enqueue the last worker (SaveImageToFileWorker) and to start the work, it is necessary to call the enqueue method in our continuation object.
The constraint RequiresCharging is added for the last worker, moreover it is also possible to add more constraints like StorageNotLow, DeviceIdle or BatteryNotLow. In this way, if any of these constraints are not fulfilled, the Worker will not initiate. Additionally, if it is desired to cancel any WorkRequest or work chain, it can be accomplished by calling the method cancelUniqueWork as shown in the Snippet 6.
Snippet 6. Cancel Work method
We have learned that the WorkManager is a powerful tool. It can help us in many situations where we want to run background tasks in a clean mode by using the device resources in the best possible way. It has some advantages like the fact that it will continue its execution even if the application is closed or the device restarts. Nevertheless, as we said at the beginning of the article, we can not use the WorkManager in every background process. We have to be sure that we really need it, otherwise it may be better to choose a different option like the Foreground service, the DownloadManager or the AlarmManager. Another useful feature is the possibility to chain works using WorkContinuation, which can allow complex graphs or combine a list of WorkRequests. To conclude, the WorkManager is a library to run background tasks that are deferrable, it uses system health best practices and guarantee background work when the work’s constraints are satisfied. As an additional tip, if you were using the Evernote jobs library, you should start moving to the WorkManager library, since it is almost a complete replace.
What we did not cover
For interested readers that want to know the details of how WorkManager works under the hood, there is this article. There you see how the threads are managed and how the library guarantees the execution of any task. Also, the same article explains how we can observe the WorkRequest status (Blocked, Enqueued, Running and Succeeded, Failed or Cancelled) using the JetPack’s library: LiveData.
References and further reading