Coroutines - Simple Examples
This is an android tutorial to help understand Coroutines using simple standalone examples.
What is a Coroutine?
It is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.
On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive.
Example 1: Kotlin Coroutines - Simulate API Data Fetching
A simple example to help you get started with Kotlin Coroutines. This example simulates downloading of data from an API and setting that data on the textview in the main thread.
The demo of what is created looks like this:
Step 1: Create Project
Start by creating an empty Android Studio
Kotlin project.
Step 2: Dependencies
As dendencies in your app/build.gradle
, add Coroutine and Coroutine-Android inside the dependencies closure:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
Step 3: Design Layout
Add button and a TextView to the xml layout:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.327" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintVertical_bias="0.176" />
</androidx.constraintlayout.widget.ConstraintLayout>
Step 4: Write Code
Start by adding imports:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Extend the AppCompatActivity
:
Define two instance fields which will represent the API call result:
This functions will set the downloaded result to textview on the main thread:
private fun setText(input: String) {
val newText = textView.text.toString() + "\n$input"
textView.text = newText
}
private suspend fun setTextOnMainThread(input: String) {
withContext(Main) {
setText(input)
}
}
The following function will simulate an API call:
private suspend fun fakeAPIRequest() {
val result1 = getResult1FromApi()
println("debug: $result1")
setTextOnMainThread(result1)
val result2 = getResult2FromApi()
setTextOnMainThread(result2)
}
When the button is clicked we will laucng the API call in the following onCreate()
method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
CoroutineScope(IO).launch {
fakeAPIRequest()
}
}
}
Here is the full code for the MainActivity
;
MainActivity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
private val RESULT1 = "Result#1"
private val RESULT2 = "Result#2"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
CoroutineScope(IO).launch {
fakeAPIRequest()
}
}
}
private fun setText(input: String) {
val newText = textView.text.toString() + "\n$input"
textView.text = newText
}
private suspend fun setTextOnMainThread(input: String) {
withContext(Main) {
setText(input)
}
}
private suspend fun fakeAPIRequest() {
val result1 = getResult1FromApi()
println("debug: $result1")
setTextOnMainThread(result1)
val result2 = getResult2FromApi()
setTextOnMainThread(result2)
}
private suspend fun getResult1FromApi(): String {
logThread("getResult1FromApi")
delay(100)
return RESULT1
}
private suspend fun getResult2FromApi(): String {
logThread("getResult2FromApi")
delay(50)
return RESULT2
}
private fun logThread(methodName: String) {
println("debug: ${methodName}: ${Thread.currentThread().name}")
}
}
Run
Copy the code or download it in the link below, build and run.
Reference
Here are the reference links:
Number | Link |
---|---|
1. | Download Example |
2. | Follow code author |
Example 2: Kotlin Coroutines - UI Update Example
This is a simple example showing how to use coroutines to perform background tasks while updating progress to the UI in parallel on main UI Thread.
Here is the demo screenshot of what is created:
Step 1: Create Project
Create a Kotlin Project in Android Studio. The project has to be a Kotlin one since we intend to use Coroutines.
Step 2: Add Dependencies
Then in your app/build.gradle
add the following dependencies:
implementation 'com.akexorcist:RoundCornerProgressBar:2.0.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.21"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"
The round corner progressbar is a third party library and is subsitutable.
Step 3: Design layout
The layout is our main activity layout. Add the 3 progressbars as well as a button:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp"
tools:context="com.elyeproj.democoroutinesrace.MainActivity">
<com.akexorcist.roundcornerprogressbar.RoundCornerProgressBar
android:id="@+id/progressBarRed"
android:tooltipText="Red"
android:layout_width="match_parent"
android:layout_height="50dp"
app:rcProgress="0.0"
app:rcMax="1000.0"
app:rcRadius="10dp"
app:rcBackgroundPadding="2dp"
app:rcProgressColor="#f00"/>
<com.akexorcist.roundcornerprogressbar.RoundCornerProgressBar
android:id="@+id/progressBarGreen"
android:tooltipText="Green"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="50dp"
app:rcProgress="0.0"
app:rcMax="1000.0"
app:rcRadius="10dp"
app:rcBackgroundPadding="2dp"
app:rcProgressColor="#0f0" />
<com.akexorcist.roundcornerprogressbar.RoundCornerProgressBar
android:id="@+id/progressBarBlue"
android:tooltipText="Blue"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="50dp"
app:rcProgress="0.0"
app:rcMax="1000.0"
app:rcRadius="10dp"
app:rcBackgroundPadding="2dp"
app:rcProgressColor="#00f" />
<Button
android:id="@+id/buttonStart"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start" />
</LinearLayout>
Step 4: Implement a Continuation Interface
Then override the resume()
and resumeWithException
functions:
Here is the full class:
AndroidContinuation.kt
import android.os.Handler
import android.os.Looper
import kotlin.coroutines.experimental.AbstractCoroutineContextElement
import kotlin.coroutines.experimental.Continuation
import kotlin.coroutines.experimental.ContinuationInterceptor
private class AndroidContinuation<in T>(val cont: Continuation<T>) : Continuation<T> by cont {
override fun resume(value: T) {
if (Looper.myLooper() == Looper.getMainLooper()) cont.resume(value)
else Handler(Looper.getMainLooper()).post { cont.resume(value) }
}
override fun resumeWithException(exception: Throwable) {
if (Looper.myLooper() == Looper.getMainLooper()) cont.resumeWithException(exception)
else Handler(Looper.getMainLooper()).post { cont.resumeWithException(exception) }
}
}
/**
* Android context, provides an AndroidContinuation, executes everything on the UI Thread
*/
object Android : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
AndroidContinuation(continuation)
}
Step 5: Do Work
Last but not least we come to our main activity.
As instance fields define a couple of variables, some to represent the Job tobe performed and a boolean to represent the completion statis of those jobs.
private var raceEnd = false
private var greenJob: Job? = null
private var redJob: Job? = null
private var blueJob: Job? = null
Here is the function to initiate the job:
private suspend fun startRunning(progressBar: RoundCornerProgressBar) {
progressBar.progress = 0f
while (progressBar.progress < 1000 && !raceEnd) {
delay(10)
progressBar.progress += (1..10).random()
}
if (!raceEnd) {
raceEnd = true
Toast.makeText(this, "${progressBar.tooltipText} won!", Toast.LENGTH_SHORT).show()
}
}
Here is the function to update the progressbars:
private fun startUpdate() {
resetRun()
greenJob = launch(Android) {
startRunning(progressBarGreen)
}
redJob = launch(Android) {
startRunning(progressBarRed)
}
blueJob =launch(Android) {
startRunning(progressBarBlue)
}
}
Here is the function to cancel the jobs:
Here is the full code:
MainActivity.kt
package com.elyeproj.democoroutinesrace
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
import com.akexorcist.roundcornerprogressbar.RoundCornerProgressBar
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import java.util.*
class MainActivity : AppCompatActivity() {
private var raceEnd = false
private var greenJob: Job? = null
private var redJob: Job? = null
private var blueJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonStart.setOnClickListener {
startUpdate()
}
}
override fun onDestroy() {
super.onDestroy()
resetRun()
}
private fun startUpdate() {
resetRun()
greenJob = launch(Android) {
startRunning(progressBarGreen)
}
redJob = launch(Android) {
startRunning(progressBarRed)
}
blueJob =launch(Android) {
startRunning(progressBarBlue)
}
}
private suspend fun startRunning(progressBar: RoundCornerProgressBar) {
progressBar.progress = 0f
while (progressBar.progress < 1000 && !raceEnd) {
delay(10)
progressBar.progress += (1..10).random()
}
if (!raceEnd) {
raceEnd = true
Toast.makeText(this, "${progressBar.tooltipText} won!", Toast.LENGTH_SHORT).show()
}
}
fun ClosedRange<Int>.random() =
Random().nextInt(endInclusive - start) + start
private fun resetRun() {
raceEnd = false
greenJob?.cancel()
blueJob?.cancel()
redJob?.cancel()
}
}
Step 6: Run
Lastly run the project.
Reference
Download the code below:
Number | Link |
---|---|
1. | Download code |
2. | Follow code author |