DEV Community

Cover image for Using Kotlin Coroutines in Android
Neeyat Lotlikar
Neeyat Lotlikar

Posted on

Using Kotlin Coroutines in Android

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)

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...
}
Enter fullscreen mode Exit fullscreen mode

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:

CoroutineScope(Dispatchers.IO).launch {
    // Code performing IO and network operations
}
Enter fullscreen mode Exit fullscreen mode

Here, a new coroutine with the IO context is created.

To switch to the main context:

withContext(Dispatchers.Main) {
    // Code to execute on the UI thread
}
Enter fullscreen mode Exit fullscreen mode

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.

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()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Get the full app code here.

GitHub logo neeyatl / HTML-WebPage-Downloader

An application that downloads a web page given it's web address or it's URL.

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

Top comments (0)