Background Processing in Android (Part 2)

In our previous Background Processing posts, Julian Falcionelli talked about some options that we have to do background tasks in Android. In this post we will explore other alternatives, mostly provided by the Android Framework, mainly focused on running long background operations, until the most recent, provided by the Kotlin team, that allows us to quickly handler background task in an efficient way.

Ezequiel Excoffon
May 17 · 11 min read

Alarm Manager

Alarm Manager is one of the Android Services that we have to execute tasks in the background, perhaps the simplest to use, with the peculiarity that we have to define the time in which we want the task to be executed.

Some of the characteristics of the Alarm Manager are:

  • The alarm can be triggered even when the device is sleeping or the application is not running.
  • When the alarm fires an intent, could be combined with a broadcast receiver to start a new action like run a new Service (JobService) or to just update the UI.

We have two ways to schedule an Alarm:

  • By using the Elapsed Real Time that is the time since the last boot.
  • By using the Real Time Clock (RTC) that is the clock time in UTC.

As you can see in the previous example, setAlarm(context: Context) is a method where an alarm is scheduled for 6 seconds in the future. It also sets the type of Alarm and the PendingIntent.

The first one was set as RTC_WAKEUP, this means it should wake up the device within a specific time in UTC. The second one specified the action that will be performed when the alarm is fired. cancelAlarm(context: Context), cancels the alarm that has been scheduled sending as a parameter the PendingIntent that was sent in the previous method so all the alarms that match with the same PendingIntent will be cancelled. Finally, the onReceive(context: Context?, intent: Intent?) method overridden logs that the alarm was fired.

Problems:

  • All scheduled alarms will be executed until we stop them by calling the cancel() method of the AlarmManager class, or when the device is rebooted. So if you’ve configured alarms to work even if the device is rebooted, you should use the android.intent.action.BOOT_COMPLETED to detect when the device is turned on and re-configure all the alarms.
  • From the API 19 (KitKat) the alarm’s trigger is unclear, because the OS will shift alarms in order to minimize wake ups and battery use. For specific cases in which the alarm should be triggered on specific times use the setWindow(type: Int, windowStartMillis: Long, windowLengthMillis: Long, operation: PendingIntent) or setExact(type: Int, triggerAtMillis: Long, operation: PendingIntent) methods. When the API level of the application is lower than 19 the time of the alarm will be exactly the same as the one delivered.

Job Scheduler

This API was introduced on the Lollipop version (API Level 21).

The reason why this new way to perform background operations exists is that Unlimited Services (Services running for ever) are not allowed since Lollipop. The indiscriminate use of them were causing Wakelocks and, therefore, a lot of performance issues, battery drains. If you want to learn what Wakelocks are and how can you them, check this post.

Different than the Alarm Manager, to run a Job, some conditions must be met rather than setting a specific time as we were doing in the previous alternative.

Some of those conditions are:

  • Check the network type (Wifi, Mobile Data).
  • Check if the device is idle.
  • Check the battery status.
  • Check the storage of the device is not low.
  • Check if the device is charging.
  • And more.

To implement a Job Service follow the next steps:

1- Declare the Service in the Android Manifest: First you have to add the service on the AndroidManifest file. Also, you have to add the permission android.permission.BIND_JOB_SERVICE in order to protect the JobService. If you don’t put that permission the service will be ignored by the system.

2- Define the run criteria: In this step you need to create a JobInfo object where you indicate the criteria for run the job, and also you have to send the ID of the Job and a JobService object.

In the example, there are two methods to set the conditions that the job has to meet in order to run it. The method setRequiresDeviceIdle(requiresDeviceIdle: Boolean) ensures that the job will not run if the device is in active use in case that we sent true. By default the value is false, so the job will run even when someone is interacting with the device. And the other method setRequiredNetworkType(networkType: Int) sets the kind of network requires the Job. In this case we use JobInfo.NETWORK_TYPE_ANY.

3- Handling the JobService lifecycle: As we say in the previous step, you need to create a new class which should extend from the JobService. There are a few methods that you should override or you can use for manage the behaviour of the Job.

  • onStartJob(jobParameters: JobParameters?): Boolean: This method is called by the system when it’s time to be executed and all the conditions were met. The thing here is that the Service is running on the Main Thread, so the work should be simple and don’t make any I/O block or you should do the work on a background thread. You could use RX inside to handler this. If you have to still doing some work you should return true, and if in other cases the work is done you have to return false.
  • onStopJob(jobParameters: JobParameters?): Boolean: This method is also called by the system when was canceled before it finish. This could be because the conditions specified on the JobInfo object are no longer met. As onStartJob, we should return a boolean value. If we return true the system will reschedule the job, but if we return false the job will be dropped.
  • jobFinished(jobParameters: JobParameters?, wantReschedule: Boolean): If the onStartJob() returns true, this method should be called explicitly when the job is completed. The second parameter of this method is to indicate if you want to reschedule the job.

Problems:

  • We can not know with certainty the exact moment in time when the service is going to run, so it is difficult to debug / test. Obviously it depends on the conditions that we configure, but many of these configurations depend on the current state of the device.
  • It is useful when you have to do operations when the device is not in use.
  • As was introduced on API 21, you can only use it with that API Level or higher. In case that your app supports a minor level than that, you should implement other approach for that case.

Work Manager

With the new release of Android Jetpack, it introduces a new way to manage your background work that is called WorkManager. This API runs the background work that is deferrable. This means that the work could be postponed for do it in the future, so the task is not necessary to run immediately. The execution of this work depends on some constraints that should be met (similar as we were doing in the JobServices). Also the work to do is guaranteed, so the task will run even if you navigate away from the app, or the device restarts. Some of the benefits using Work Manager are:

  • It’s compatible to API level 14.
  • Runs with or without Google Play Services.
  • Support for one-off and periodic tasks.
  • Support for some constraints like network conditions, charging status and storage space like in JobServices.

Let's try it: First you need to extend the Worker class. In case you are using RxJava/RxAndroid you can extend directly from the RxWorker class or from CoroutineWorker class if you are using Kotlin Coroutines (which I’ll explain later on).

The method that you should implement of the Worker class is where you define the task that you want to run on background. This method will return the result of the work success, failure or retry if you want to retry the work in case that work finds a transient failure, like the device lost the network connection. So the work will be retried with some backoff criteria specified in WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit).

So after you implement that class, you should create a work request for enqueue work and run it. There are two types of Work Request, and both depends if the work will only be executed one time (OneTimeWorkRequest) or will be repeated on a cycle (PeriodicWorkRequest). For each work request you can add some constraints, specify the input data of the work, and set the back off criteria if the work can be retried if it fails.

After that you can enqueue it by calling WorkManager.getInstance().enqueue.(workRequest).

In case that you want to update the UI when the work is finished you can use the method WorkManager.getInstance().getWorkInfoByIdLiveData(id: UUID). This method gets the work info, so it notifies about the state of the work and also sends optional output data.

One interesting feature of the Work Manager is that it allows us to easily concatenate tasks to run them parallel or sequentially. For this you can use the WorkManager.getInstance().beginWith(oneTimeWorkRequest: OneTimeWorkRequest) and workContinuation.then(oneTimeWorkRequest: OneTimeWorkRequest) methods.

Problems:

  • As in the Job Schedulers, we cannot know exactly when the service is going to run, so it is difficult to debug / test.

Kotlin Coroutines

By the Kotlin team, Coroutines are like “light-weight threads”. This means that the creation of a coroutine is very cheap. This alternative allows us to work with asynchronous code with a synchronic way, so we can read the code as sequential. By doing this we can read the code with a more natural way and avoid the known "Callback Hell", that with Rx and Lambdas you see a more elegant way for fix this problem in the previous post. The way to work with Kotlin Coroutines is by using Suspending Functions. Let's take a look at them!

Suspend Functions

Suspend Functions are the base of Coroutines. All our tasks that need background processing are going to run inside an Suspend Function. In order to fully use this, we actually need to call this kind of functions inside another Suspend Function or Coroutine. The main thing here is that Suspending function is suspended when calls another suspend function, and will be resumed when that function called finish. The easy way to do this is by using the Coroutine Builders, lets move to them.

Coroutine Builders

This are functions that allow us to quickly start Coroutines from non-suspending functions. The Coroutine Builder that we should select depends on what we want to do, and how we want be executed the suspending functions. Those are:

  • runBlocking: starts a coroutine, blocks the current thread and waits until finish with all the tasks inside it. It's useful for test implementation of suspending functions.
  • launch: launches a coroutine without blocking the current thread and returns a Job that is a reference to the coroutine.
  • async: starts a coroutine and returns a Deferred object that represents a promise to provide a result in the future. To get the results you can use the method await from the Deferred object. This method await for completion of the value without blocking the current thread.

In the previous example, after showing the progress bar we are calling a suspending function with async that returns a Deferred object of String. Then, the list of Soccer Player is cleaned and after it we wait for the result from the Deferred object soccerPlayer. The progress bar is still showing until the await finishes and after that is hidden.

Coroutine dispatchers

The dispatchers are a coroutine context that determine in which thread or threads the coroutine can be used to execute the code. The main dispatchers are:

  • Dispatchers.Main: This dispatcher is confined to the Main Thread of the OS. In the case of Android is the UI thread.
  • Dispatchers.Default: It is used by all the standard builders like launch or async if the dispatcher is not specified. It is used to run tasks that the use of the CPU resources are really intensive.
  • Dispatchers.Unconfined: It is not restricted to any specific thread, so will use the first available thread.
  • Dispatchers.IO: You should use it for run input/output operations. Tasks like server request or access to database are cases that you can use this dispatcher.
  • newSingleThreadContext: It creates a thread for the coroutine to run. A thread dedicated for a specific task is a very expensive resource, so it is advisable to close it when is no longer needed.
  • newFixedThreadPoolContext: Different than the previous dispatcher, in this case creates a fixed size of thread-pool.

Coroutine context

The coroutine context is a set of various elements that defines how the coroutine will be executed. One of the configurations that can be set is the dispatcher of the coroutine. A way to set the dispatcher is sending it as a parameter when it calls the CoroutineBuilder, so all the Coroutine will execute on that dispatcher. If the coroutine needs to change of dispatcher, you can use the method withContext(). This method is a suspending function that changes the context. Then you’ll be able to run a part of the code with a different context than the Coroutine Builder.

CoroutineScope

The CoroutineScope class provide us a way for manipulate the contexts and jobs of the coroutines that should be linked with the lifecycle of the activity. This feature is really useful when for example a coroutine that is related with the UI (e.g. get a list of soccer players) is running and the user closes the currently activity. Stuff like this could make a memory leak or throw an exception. One approach for manage the lifecycle of the coroutine is extending the CoroutineScope class.

In this example, the MainActivity class extends the CoroutineScope and overrides the coroutineContext property. The operation plus(+) is for combine more than one context, so in this case we are combining two important things: the dispatcher and the job. The first thing specify the default dispatcher where the coroutines will run, and the Job is for cancel all the coroutines at any moment. In the method startCoroutine() we are using the CoroutineBuilder launch and sending as a parameter the Dispatchers.IO for switch to the IO thread. It is because as we set the Main dispatcher as the default one the coroutine will run on the Main Thread. On other hand, in the onDestroy() method the job is cancelled because the activity was killed.

Channels (experimental)

If you used to use Rx, an alternative that Kotlin Coroutine has is Channels. The Channels provide a way to transfer a stream of values. This feature conceptually is similar to BlockingQueue of Java, but the difference is that instead of blocking the operation for send the data and receive it, those operations are suspended.

In the next example you can see an extension function that produces integer numbers from 1 to a limit that is sent as a parameter. The Coroutine Builder produce launches a new coroutine that produce a stream with the numbers generated that are sent to a channel. This coroutine builder returns a reference to the coroutine as a ReceiveChannel, so this can be used for receive the elements produced by the coroutine. After receive the stream of numbers, the consumer can filter only for the pair numbers.

Kotlin is a bit topic to talk about, you can find more about Kotlin Coroutines in:

Kotlin Coroutine Guide of Kotlinlang.org

Kotlin Coroutine section of KEEP(Kotlin Evolution and Enhancement Process) repository

Conclusion

We are still using reactive programming (RxJava/RxAndroid) in our projects for the communication between the Repositories and the Presenters or ViewModels as was explained in the previous post made by Julián. Beyond the readability that we win with Kotlin Coroutines, the operators that the Rx extensions provide to us are really powerful and useful. We try to not mix alternatives like Rx and Coroutines, and use only one for one purpose, or in the case that we need them, them should be clearly separated between the layers of the architecture in the project.

Happy coding !

Thanks to Julián Falcionelli

Ezequiel Excoffon

Written by

Major League

The Major League Engineering and Design Blog

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade