DEV Community

Cover image for Being fishy with Android - Background work
Levi Albuquerque
Levi Albuquerque

Posted on

Being fishy with Android - Background work

Here I am again talking about threads and processes and background stuff :)

One of the first things you learn when doing Android (or even iOS) is to keep heavy load processing off the Main Thread. The Main Thread is too busy drawing those beautiful animations you love seeing and it doesn't like being stopped to do mundane things such as network requests. It will complain, mark my words.

There are many ways of doing background work in Android, many APIs have been developed (and deprecated, right Loaders?) and it's always good to remember who they are and when they can help you. This is going to be very quick, I promise :)

AsyncTasks

Let's start with the first one you've heard about, AsyncTasks. You hear about it first because it's one of the easier APIs to run work off the Main Thread and then easily publish the results of such work back to it. The usage is quite straightforward. I'll use the canonical example of an AsyncTask that performs a download operation whilst showing a progress bar. First, you have to think about three things:

1 - The input type of the task you want to run. For me, I'll take an array of URL instances which contains the Urls for the files I wished to download.
2 - The type of intermediate output you'd like to show. In my case that's an integer representing the percentage of the files that has been downloaded so far so I can update my progress bar.
3 - The third thing you need to think about is the type produced by your task. In my case it will be a Long because I'd like to know how many bytes were downloaded at the end of the task.

class DownloadFilesAsyncTask : AsyncTask<URL, Int, Long>() {

    override fun onPreExecute() {
        showProgressBar()
    }

    override fun doInBackground(vararg urls: URL?): Long {
        var downloadedSize: Long = 0
        var filesToDownload = 0

        for (i in 0 until filesToDownload) {
            downloadedSize += downloadFileWithSize(urls[0])
            publishProgress((i / urls.size * 100))
        }

        return downloadedSize
    }

    override fun onProgressUpdate(vararg values: Int?) {
        setValueInProgressBar(values[0])
    }

    override fun onPostExecute(result: Long?) {
        closeProgressBar()
        showDialog(result)
    }
Enter fullscreen mode Exit fullscreen mode

This is a rather simplified version so you can get the idea: we've created a custom class that extends AsynTask. Usually you'll want to implement this as an inner class of the Activity that requires the task to be performed. Up top we've defined the three things we had in mind when creating this AsyncTask: the input type (URL), the Progress output type (Int) and the final output type(Long). From that we just necessarily need to implement one single method: doInBackground, but I've written some of the others so we could see all the capabilities:

  • onPreExecute will run in the Main Thread, this means you can access (touch) any view in the activity. Use this to prepare the UI for the task ahead.
  • doInBackground, as the name says will be run in a different Thread. You need to peform you work and return a value in the end. Here you can use the publishProgress to output a progress on your task to the UI.
  • onProgressUpdate is called anytime you call publishProgress inside doInBackground and this method is executed in the UI thread.
  • onPostExecute is called when your task finishes. Here you can show in the UI the final result from your task.

To use it, you must call it in the UI thread:

DownloadFilesAsyncTask().execute(urls)
Enter fullscreen mode Exit fullscreen mode

AsyncTasks are nice and all but this tight coupling with the UI you gain with the easy way of using it can cause a couple of headaches when not cared for carefully. Just to mention one of them imagine if you start running a very long AsyncTask and the user rotates the device... You know where I'm getting at right? The activity will be destroyed, the AsyncTask will finish at some point and it'll try to access the views that no longer exist, boom! Your app crashes... So, be careful when using them. As the docs say: "AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.)"

JobScheduler

As the name says this is the go to API for scheduling tasks in Android. Another option is AlarmManager which we'll talk a bit about later, but the JS APIs were launched in Lolipop therefore they contain far more interesting and complete methods for running tasks.

Usually we'd use JobScheduler in the following cases:

  1. When we'd like to execute tasks within a certain system constraint. This means for instance that JS is ideal for when we'd like to run tasks only when the device is plugged or when we have wifi connection.

  2. Tasks that can be deferred. The JS API will defer tasks for when is most appropriate to run them, this means that such tasks can't be time critical.

  3. Periodic tasks.

The first step to use this API is to create an instance of JobService to handle your work.


class MyJobService : JobService() {
    override fun onStopJob(params: JobParameters?): Boolean {
    }

    override fun onStartJob(params: JobParameters?): Boolean {
    }
}
Enter fullscreen mode Exit fullscreen mode

As this is a Service it will need to be defined in the Manifest file as well:

<service
   android:name=".MyJobService"
   android:permission="android.permission.BIND_JOB_SERVICE"/>
Enter fullscreen mode Exit fullscreen mode

As you could see the JobService requires us to override two methods:

  • onStartJob: As the name says this method is called as soon as Android decides to start running our service. Note that the job will run on the Main Thread unless otherwise specified, that means you'll need to put any heavy lifting work on another thread. This method should return true if the job is to keep running. The job will keep running until you call jobFinished() or until any of the constraints you set to run the job can't be satisfied.
  • onStopJob: This method is called only when the service is cancelled before it's finished. This can happen because of multiple reasons, for instance, because the constraints it was set to run against can't be satisfied. If you return true in this method the job will be rescheduled.

Once you've implemented the service, the next step is to start the job. To do so you need to create a JobInfo instance through a builder. In this JobInfo you can specify any constraints that you might have to run this job, for example you can set it to run only when the device is connected to the power, or when it is connected to the WiFi.

 val componentName = ComponentName(this, MyJobService::class.java)
        val jobInfo = JobInfo.Builder(123, componentName)
            .setRequiresCharging(true)
            .build()
Enter fullscreen mode Exit fullscreen mode

To finally schedule your job is quite simple:

 val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
        jobScheduler.schedule(jobInfo)
Enter fullscreen mode Exit fullscreen mode

Please note that whilst the service is running, your app will keep a wakelock (will prevent the device from going to sleep). Therefore if you forget to finish it (either automatically or manually) it will drain your battery, so never forget to do so.

Firebase JobDispatcher

Did you notice something odd about the JobScheduler API? It was released for Lollipop+ devices, so we didn't actually had an option for scheduling background work for pre-Lollipop devices. Well, we didn't. The Firebase JobDispatcher library was created exactly for that reason. To use it we'd need to include the dependency:

implementation 'com.firebase:firebase-jobdispatcher:0.8.5'
Enter fullscreen mode Exit fullscreen mode

We'd need to create an instance of JobService, like the one before, overriding similar methods. But we'll use the JobService from the library. We add the service to the manifest just like before:

<service
    android:exported="false"
    android:name=".MyJobService">
    <intent-filter>
        <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
    </intent-filter>
</service>
Enter fullscreen mode Exit fullscreen mode

Things are a bit different when we need to schedule the job. First we create a dispatcher:

 val dispatcher = FirebaseJobDispatcher(GooglePlayDriver(context))
Enter fullscreen mode Exit fullscreen mode

Then we use the dispatcher to both create the Job (here we can add constraints just like before) and schedule it:

        val myJob = dispatcher.newJobBuilder()
            .setService(MyJobService::class.java)
            .setTag("tag")
            .build()

        dispatcher.mustSchedule(myJob)
Enter fullscreen mode Exit fullscreen mode

To cancel a specific job we simply call cancel on the dispatcher passing the tag we've set when we created the job:

dispatcher.cancel("tag");
Enter fullscreen mode Exit fullscreen mode

AlarmManager

AlarmManager is as old as Android itself, this API is very useful for firing up tasks at a specific time, periodically. It should be used only when the task must execute at a specific time and you don't need the conditions that the JobScheduler provide. I won't be providing an example for it, but the official page is a great place to retrieve examples. WorkManager would be the best option to use in most cases, and AlarmManager is one of the composing APIs of WorkManager.

I'm not going to talk about Services or WorkManager because I'm dedicating different posts in the near future just for them :)

Oldest comments (1)

Collapse
 
tomavelev profile image
Toma

Keeping stuff outside of UI Thread is a basic thing for any kind of UI programming. The Mobile OSes just pointed out how deeply important it is - as on the backend, most devs rarely worry about side-way execution and on web frontend - the heaviest operation in most cases (ajax) is 99% used asynchronously.
About Android - which API to use is a little bit more about the desired end goal (User experience/The business requirement). From developer perspective, I'm sad about ever-changing ways of hot to do things forced by Google, but I am happy from user perspective :)