Using Kotlin Coroutines in Android
Tags: kotlin, android, tutorial
Published: 2020-06-10 05:50:00

As we all developers know, conducting heavy processing tasks on the main/UI thread can make our applications lag. When this happens, the users become less patient, and eventually after a long exposure may decide to get rid of the app for good.

Previously, we used `AsyncTask` to deal with this problem but this approach did have some limitations. For instance, `AsyncTask` keeps running in the background even after the activity has finished and it has to be stopped manually, also any exceptions caused have to be handled manually, it was unstable, and it is also known to cause memory leaks to name a few. For these reasons, Google is deprecating the `AsyncTask` API in Android 11.

Now, with that being said, here's why Kotlin Coroutines are a great Alternative. Coroutines are easy to read and understand, they are lightweight, and many coroutines can be run together on the same thread and hence, avoids delays caused due to thread precedence in the processor.

In this post, I will show you how I use coroutines in an Android application development environment. I am in no way an expert but I am good at using them to serve the purpose of background processing. My goal is to covey how one can use coroutines instead of AsyncTask to accomplish the same jobs. So, let's begin!

To enable coroutines in Android, you must add the following dependencies in your build.gradle app module file.

`build.gradle(app)`
``` groovy
dependencies {
    // Other dependencies...
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
    // Other dependencies...
}
```

Now, let's go through some basic coroutines terminology.

`CoroutineScope` - defines a scope in which new coroutines run
`launch` - a builder that creates and helps run coroutines
`Dispatchers` - they are values which can be used to switch between different kinds of coroutine contexts
There are three main types of coroutine contexts:
1. `Dispatchers.IO` - used to perform file IO and network calls
2. `Dispatchers.Default` - It can be used for performing heavy processing tasks in the background
3. `Dispatchers.Main` - is used to switch to the main context to perform UI related operations
`withContext` - is used to switch between different coroutine contexts

Launch a new coroutine using the code:
``` kotlin
CoroutineScope(Dispatchers.IO).launch {
    // Code performing IO and network operations
}
```
Here, a new coroutine with the IO context is created.

To switch to the main context:
``` kotlin
withContext(Dispatchers.Main) {
    // Code to execute on the UI thread
}
```
In the above code, the `withContext(Dispatchers.Main)` block runs in the UI thread and you can update the UI in this thread. Trying to do so in IO or Default thread will give an error because Android does not support multithreaded UI. User interface changes can only be made in the thread so named.

I have created an app to download a web page. This app uses a function employing coroutines to do so and I will use it as a reference for explaining their use.

``` kotlin
fun downloadWebPage(
        urlString: String, progressTextView: TextView, circularProgress: ProgressBar,
        fileName: String = "/storage/emulated/0/Download/download_${System.currentTimeMillis()}.html"
) {
    CoroutineScope(Dispatchers.IO).launch {
        try {
            // Create a URL object
            val url = URL(urlString)
            // Create a buffered reader object using the url object
            val reader = url.openStream().bufferedReader()

            // Enter filename in which you want to download
            val downloadFile = File(fileName).also { it.createNewFile() }
            // Create a buffered writer object for the file
            val writer = FileWriter(downloadFile).buffered()

            // read and write each line from the stream till the end
            var line: String
            while (reader.readLine().also { line = it?.toString() ?: "" } != null)
                writer.write(line)

            // Close all open streams
            reader.close()
            writer.close()

            // Update UI for download is successful
            withContext(Dispatchers.Main) {
                circularProgress.visibility = View.GONE
                progressTextView.apply {
                    text = context.getString(R.string.download_successful)
                    setTextColor(context.getColor(android.R.color.holo_green_light)
                }
                
                Snackbar.make(
                        progressTextView,
                        R.string.file_downloaded_successfully,
                        Snackbar.LENGTH_LONG
                ).setAction(R.string.view) {
                    it.context.startActivity(Intent.createChooser(
                            Intent(Intent.ACTION_VIEW).apply {
                                setDataAndType(downloadFile.toUri(),
                                        "text/plain")
                            },
                            progressTextView.context
                                    .getString(R.string.open_using)
                    ))
                }.show()
            }

        } catch (e: Exception) {
            // Update UI for download has failed
            withContext(Dispatchers.Main) {
                when (e) {
                    is MalformedURLException ->
                        Snackbar.make(
                            progressTextView,
                            R.string.malformed_url,
                            Snackbar.LENGTH_SHORT
                        ).show()
                    else ->
                        Snackbar.make(
                            progressTextView,
                            R.string.download_failed,
                            Snackbar.LENGTH_SHORT
                        ).show()
                }

                val incompleteFile = File(fileName)
                if (incompleteFile.exists()) incompleteFile.delete()

                circularProgress.visibility = View.GONE
                progressTextView.apply {
                    text = context.getString(R.string.download_failed)
                    setTextColor(context.getColor(android.R.color.holo_red_light))
                }

                e.printStackTrace()
            }
        }
    }
}
```
The code creates a buffered reader and a buffered file writer respectively. I'm using the while loop to read and write data to the file line-wise.

The UI is updated to show success when no exceptions are caught. Failure is discerned when an exception has occurred thereby interrupting the download. In this case, the UI is updated conveying the same and the incomplete file is deleted.

See the app working. UI is interactive.
{% youtube x7OMjovhx-8 %}

Get the full app code here.
{% github Aurum1611/HTML-WebPage-Downloader %}

I hope this is helpful. Leave any comments and suggestions down below. Happy coding!