Processing background work and tasks is something that’s used in almost all mobile apps. Keeping the UI or main thread free from too many complex operations and offloading all the heavy lifting to background threads isn’t only considered a good development practice, but it’s also crucial if you want to make an app that provides a fluid and engaging user experience.
In Android, since its inception, developers have been using AsyncTask to achieve this. Compared to classic Java threads, AsyncTask provides helper UI wrappers around threads that allow developers to easily update their app’s UI based on the results from background operations.
While helpful, developers haven’t been extremely happy, given a number of issues surrounding AsyncTask—instability, memory leaks, and code smell to name a few.
The Android team has recently deprecated AsyncTask. While that doesn’t mean it’s being removed from Android, it might be removed in future Android releases. As such, developers should start migrating to an alternative offering.
I have been personally using AsyncTask in the AfterShoot app, so I figured that it might be a good time I look at the alternate solutions and coroutines in Kotlin ended up being my choice!
Let’s look at the offerings provided by Kotlin and how we can use them to replace AsyncTask in our apps.
Introduction and Overview
Coroutines are not a new concept introduced by Kotlin. As a matter of fact, they’ve existed in a lot of programming languages for quite some time; it’s just that Kotlin is the first one that allowed Android developers to use them in their apps.
Let’s see how coroutines differ from a traditional thread provided by Java.
Threads vs Coroutines
While a traditional thread allows you to run a piece of code off of the main thread, you end up with a bunch of code hidden inside callbacks, and you might not be able to scale it very well.
For example, if I want to make a network call and update the UI based on the result I receive, this is what the code for it looks like using threads:
fun makeNetworkCall(){
thread{
// network call, so run it in the background
val result = getResults()
// can't access UI elements from the background thread
MainActivity.runOnUIThread{
textView.text = result.name
}
}
}
Using coroutines, the code will look like this:
// note the suspend modifier next to the fun keyword
suspend fun makeNetworkCall(){
// the getResults() function should be a suspend function too, more on that later
val result = getResults()
textView.text = result.name
}
As you can see, the coroutine code is not only shorter, but we have fewer callbacks to deal with, making our code more readable and manageable!
Coroutines and suspended functions
In the above line, you might have noticed the suspend keyword being used before the function. The suspend keyword signifies that the method is a suspended method and should only be called from a coroutine scope (more on this later).
The getResults() method will make our actual network calls, and this is what it looks like:
suspend fun getResults() : User =
// Coroutine has mutliple dispatchers suited for different type of workloads
withContext(Dispatchers.IO) {
// Make network call
val result = okhttpClient.execute(request)
// parsing JSON is CPU intensive process, so run it on the Default Dispatcher
withContext(Dispatchers.DEFAULT){
val user = parseJson(result)
// return the user
user
}
}
To specify where the coroutines should run, Kotlin provides three dispatchers that you can use:
- Dispatchers.Main — Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work.
- Dispatchers.IO — This dispatcher is optimized to perform disk or network I/O outside of the main thread.
- Dispatchers.Default — This dispatcher is optimized to perform CPU-intensive work outside of the main thread.
In the above example, we wrapped our network call in the IO dispatchers, followed by the JSON parsing in the default dispatcher.
Let’s take a look at how we can replace the AsyncTask I’m using in the AfterShoot app using Kotlin coroutines.
Step 1: Adding the required dependencies
Before we start using coroutines, we need to add 2 dependencies to our app’s build.gradle file:
dependencies{
...
//coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
...
}
Step 2: Refactoring and removing the existing AsyncTask
Here’s the AsyncTask I’m using in my app:
class ProgressActivity : AppCompatActivity(){
class LoaderTask(private val progressActivity: ProgressActivity) : AsyncTask<List<Image>, Int, Unit>() {
override fun onPreExecute() {
super.onPreExecute()
progressActivity.progressBar.visibility = View.VISIBLE
}
override fun doInBackground(vararg images: List<Image>) {
val imageList = images[0]
imageList.forEachIndexed { index, image ->
// Read the bitmap from a local file
val bitmap = BitmapFactory.decodeFile(image.file.path)
// Resize the bitmap so that it's 224x224
val resizedImage =
Bitmap.createScaledBitmap(bitmap, progressActivity.inputImageWidth, progressActivity.inputImageHeight, true)
// Convert the bitmap to a ByteBuffer
val modelInput = progressActivity.convertBitmapToByteBuffer(resizedImage)
progressActivity.interpreter.run(modelInput, progressActivity.resultArray)
// A number between 0-255 that tells the ratio that the images is overexposed
Log.d("TAG", "Overexposed : ${abs(progressActivity.resultArray[0][0].toInt())}")
// A number between 0-255 that tells the ratio that the images is good
Log.d("TAG", "Good : ${abs(progressActivity.resultArray[0][1].toInt())}")
// A number between 0-255 that tells the ratio that the images is underexposed
Log.d("TAG", "Underexposed : ${abs(progressActivity.resultArray[0][2].toInt())}")
publishProgress(index)
}
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
progressActivity.tvStatus.text = "Processing : ${values[0]} out of ${imageList.size}"
}
override fun onPostExecute(result: Unit?) {
super.onPostExecute(result)
progressActivity.startResultActivity()
progressActivity.finish()
}
}
}
As you can see, it not only contains too much boilerplate code, it also holds a reference to my ProgressActivity, which might result in memory leaks!
Let’s now refactor the doInBackground and onProgressUpdate methods by extracting their contents into a new suspended method—this is what it looks like:
private suspend fun processImage(image: Image, index: Int) {
// Model inferencing is CPU internsive task so run it on the Default dispatcher
withContext(Dispatchers.Default) {
val bitmap = BitmapFactory.decodeFile(image.file.path)
// Resize the bitmap so that it's 224x224
val resizedImage =
Bitmap.createScaledBitmap(bitmap, inputImageWidth, inputImageHeight, true)
// Convert the bitmap to a ByteBuffer
val modelInput = convertBitmapToByteBuffer(resizedImage)
interpreter.run(modelInput, resultArray)
// A number between 0-255 that tells the ratio that the images is overexposed
Log.d("TAG", "Overexposed : ${abs(resultArray[0][0].toInt())}")
// A number between 0-255 that tells the ratio that the images is good
Log.d("TAG", "Good : ${abs(resultArray[0][1].toInt())}")
// A number between 0-255 that tells the ratio that the images is underexposed
Log.d("TAG", "Underexposed : ${abs(resultArray[0][2].toInt())}")
// Since we are updating the UI, do the operation on the Main dispatcher
withContext(Dispatchers.Main) {
tvStatus.text = "Processing : ${index} out of ${imageList.size}"
}
}
}
Step 3: Starting the suspended function
Lastly, we want to start our processImage() function as soon as the app starts. You might be tempted to call it directly inside the onCreate method, but in doing so, you’ll see that code throws an error:
Suspend function ‘processImage’ should be called only from a coroutine or another suspend function
What this means is, we can’t simply call a suspended function like any other function. In order to be able to start a suspended function, we first need to create a coroutine scope.
You might be wondering what a coroutine scope is! Put simply, it’s used to start and terminate our coroutines. CoroutineScope is responsible for stopping coroutine execution when a user leaves a content area within our app. This helps to ensure that there are no memory leaks.
By default, Kotlin has a Global scope that’s used to start coroutines that are supposed to run as long as your app is running. Use of the Global scope is discouraged, and you should instead resort to a more confined, local scope.
This is how you can create a scope and start the coroutine that we created earlier:
// any coroutines launched inside this scope will run on the main thread unless stated otherwise
val uiScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_progress)
uiScope.launch {
imageList.forEachIndexed { index, image ->
processImage(image)
tvStatus.text = "Processing : ${index} out of ${imageList.size}"
}
startResultActivity()
}
}
And that’s it! As you can see, the code not only looks much cleaner, but it’s also significantly reduced (26 from 41 lines).
Step 4: Specifying the scope in the function definition
If you recall our example from the start of the blog post, it looks something like this:
// note the suspend modifier next to the fun keyword
suspend fun makeNetworkCall(){
// the getResults() function should be a suspend function too, more on that later
val result = getResults()
textView.text = result.name
}
While calling the makeNetworkCall() method, you might be tempted to wrap it inside a IO block, but doing so will crash the app as you’ll be trying to modify the UI elements (textView) from a non-UI thread!
To fix this, we can alternately define the getResults() method as follows:
// setting the launch mode during the function definition
suspend fun getResult() = Dispatchers.Default {
val result: String
// make network call
return@Default result
}
Now no matter from which scope you call the getResults() method, it will always run on the Default scope.
The complete code for the example might look as follows:
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.Main).launch{
// runs on UI thread
makeNetworkCall()
}
}
suspend fun getResult() = Dispatchers.Default {
val result: String
// make network call
return@Default result
}
// note the suspend modifier next to the fun keyword
suspend fun makeNetworkCall(){
// the getResults() function should be a suspend function too, more on that later
val result = getResults()
textView.text = result.name
}
}
Over here, the makeNetworkCall() runs on the main thread, while the getResults() method runs on the background thread!
There’s much more to coroutines, and I’d recommend that you go ahead and start using them more often in your code whenever you need to perform any background processing in your app.
If you want to learn more about coroutines, I highly recommend watching this talk from Google I/O 2019 that advocates all the benefits of using them in your app:
The code shown in the blog post can be found here:
Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.
Have feedback? Let’s connect on Twitter.
Comments 0 Responses