Fandom Engineering
Published in

Fandom Engineering

Coroutines basics

Photo by Michał Parzuchowski on Unsplash

What are coroutines?

fun task() {
functionA(1)
}
fun functionA(state: Int) {
when (state) {
1 -> {
// work on UI thread
functionB(1)
}
2 -> {
// work on UI thread
functionB(2)
}
}
}
fun functionB(state: Int) {
when (state) {
1 -> {
// work on IO thread
functionA(2)
}
2 -> {
// work on IO thread
}
}
}

Are they threads?

fun runALotOfCoroutines() = runBlocking {
repeat(100_000) { // launch 100k coroutines
launch {
// some work
}
}
}

What problem do they solve?

fun loadAndShowDataCallback() {
loadData { data -> // run on IO thread
showData(data) // pass callback
}
}
fun loadAndShowDataRx() {
loadData() // observable
.subscribeOn(Schedulers.io())
.observerOn(AndroidSchedulers.mainThread())
.subscribe { data ->
showData(data)
}
}
// don’t do this at home, just to simplify an example
fun loadAndShowDataCoroutines() {
GlobalScope.launch(Dispatchers.IO) {
// launch coroutine in IO thread
val result = loadData()
withContext(Dispatchers.Main) {
// change context to Main thread
showData(result)
}
}
}

Suspend function

fun authorizeAndShowData() = runBlocking { 
val isAuthorized = authorize()
// wait here until authorize finish
if (isAuthorized) {
val result = loadData()
// wait here until loadData finish
}
}
// declare suspend function just by suspend keyword
suspend fun authorize(): Boolean {
// make some network calls
return true
}
suspend fun loadData(): String {
// fetch data
return "result"
}

Launch and Async

fun launchAndJoin() = runBlocking {
val job = launch {
val result = suspendFun()
// wait for result
}

// wait here until a coroutine referenced by job finish
job.join() // suspend function itself
// do some further work
}
fun launchAndCancel() = runBlocking {
val parentJob = launch {
val jobA = launch {
// some work
delay(100)
}
val jobB = launch {
// some work
delay(300)
}
}
// do some time consuming work
delay(200) // suspend function itself

// cancel all cancellable work of coroutine and its children
// referenced by parentJob
parentJob.cancel()
// only jobA has finished
}
fun runSuspendFun() = runBlocking {
val resultA = suspendFunA()
val resultB = suspendFunB()
// wait for the results of both suspend functions
val finalResult = "result: $resultA $resultB"
// it takes about 400ms to get here
}
fun launchAsync() = runBlocking {
val deferredA = async {
val resultA = suspendFunA()
return@async resultA
}
// deferredA has already started

val deferredB = async(start = CoroutineStart.LAZY) {
// can be lazy started
val resultB = suspendFunB()
return@async resultB
}
// deferredB waits to start by calling start or await
deferredB.start() // to avoid sequential behaviour start here
// wait for the results of both coroutines
val finalResult =
"result: ${deferredA.await()} ${deferredB.await()}"
// it takes about 200ms to get here
}
suspend fun suspendFunA(): String {
delay(200)
return "result A"
}
suspend fun suspendFunB(): String {
delay(200)
return "result B"
}

Dispatcher

fun launchCoroutinesOnDifferentThreads() = runBlocking {
launch { // actually Default is used implicit
// expensive calculations
}
launch(Dispatchers.IO) {
// network call
}
launch(Dispatchers.Main) {
// update UI
}
launch(Dispatchers.Unconfined) {
// work on thread inherited from parent runBlocking
delay(100) // suspend point
// back to work on different thread inherited
// from suspend point
}
launch(newSingleThreadContext("OwnThread")) {
// work on newly created own thread
}
}

Context

val job = Job()val dispatcher: CoroutineDispatcher = Dispatchers.Defaultval exceptionHandler = CoroutineExceptionHandler { context, exception ->
// define what to do with caught exceptions
}
// actually this is an object of CombinedContext type
val coroutineContext : CoroutineContext = job + dispatcher + exceptionHandler
fun launchCoroutineInExplicitContext() = runBlocking {
launch(coroutineContext) {
// some work
withContext(Dispatchers.Main) {
// some UI related work
}
}
}

Scope

class SomeClass {

private lateinit var scope: CoroutineScope

fun create() {
// can't just call launch, no scope provided
scope = CoroutineScope(Dispatchers.Main).launch {
// now it's possible to run a coroutine
}
}
fun destroy() {
scope.cancel()
}
}
class ScopeActivity : AppCompatActivity(), CoroutineScope {

override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// call launch is possible because
// Activity is CoroutineScope itself
launch {
}
}

override fun onDestroy() {
super.onDestroy()
cancel() // cancel the whole scope - all coroutines within
}
}
// provide CoroutineScope by delegate
class ScopeDelegateActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch { }
}
}
class LifecycleScopeActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
// scope bounded to the lifecycle of Activity
}
}
}
class LifecycleScopeViewModel : ViewModel() { init {
viewModelScope.launch {
// scope bounded to the lifecycle of ViewModel
}
}
}

Cancellation

fun cancelOnlySuspend() = runBlocking {
val cancelable = launch {
repeat(10) {
suspendWork()
}
}
val notCancelable = launch {
repeat(10) {
notSuspendWork()
}
}
delay(25)
cancelable.cancel() // suspendWork called 3 times
notCancelable.cancel() // notSuspendWork called 10 times
}
fun checkCancelManually() = runBlocking {
val checkIsActive = launch {
repeat(10) {
// check periodically is the scope still active
// or has been cancelled

// or use ensureActive to throw CancellationException
if (isActive) {
notSuspendWork()
// complete this task even if
// cancel has been called during the work
}
}
}
val callYield = launch {
repeat(10) {
notSuspendWork()
yield() // periodically suspend the work
}
}
delay(25)
checkIsActive.cancel() // suspendWork called 3 times
callYield.cancel() // suspendWork called 3 times
}
fun cancelByTimeout() = runBlocking {
// throws TimeoutCancellationException
withTimeout(25) {
repeat(10) {
suspendWork()
}
}
// returns null instead of throwing an exception
withTimeoutOrNull(25) {
repeat(10) {
suspendWork()
}
}
// both blocks called 3 times
}
fun clearOnFinally() = runBlocking {
val cancelable = launch {
try {
repeat(10) {
suspendWork()
}
} finally {
// use only non suspending functions
withContext(NonCancellable) {
// here you can use suspending functions
}
}
}
delay(25)
cancelable.cancel() // throw a CancellationException
}
suspend fun suspendWork() {
delay(10)
}
fun notSuspendWork() {
Thread.sleep(10)
}

Exception

fun useExceptionHandler() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
// GlobalScope so root coroutine
throw NullPointerException()
}
val deferred = GlobalScope.async(exceptionHandler) {
// root coroutine
throw IndexOutOfBoundsException()
}
job.join()
deferred.join()
// exceptionHandler only caught an exception from job
deferred.await()
// now the exception is thrown
// and not caught by exceptionHandler
}
fun tryCatchExceptions() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
try {
throw NullPointerException()
} catch (e: Exception) {
// some cleaning work
}

}
val deferred = GlobalScope.async(exceptionHandler) {
try {
throw IndexOutOfBoundsException()
} catch (e: Exception) {
// some cleaning work
}
}
job.join()
deferred.await()
// exceptionHandler didn't catch any exception
// because no one was uncaught
}
fun handleExceptionWhenAllChildrenTerminate() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
val job = GlobalScope.launch(exceptionHandler) {
launch {
try {
delay(1000) // some time consuming work
} finally {
withContext(NonCancellable) {
// a coroutine is cancelling, do some work
delay(25)
// more work
}
}
}
launch {
delay(10)
throw NullPointerException()
}
}
job.join()
// exceptionHandler handled an exception after 25ms
// when all children are cancelled
}
fun supervisorJob() = runBlocking {
val supervisorJob = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisorJob)) {
val childA = launch {
delay(10)
throw NullPointerException()
}
val childB = launch {
delay(1000)
// some work
}
// childA cancelled by an exception
childA.join()
delay(25)
// childB is still working, not canceled by childA fail
supervisorJob.cancel()
// now childB is cancelled by parent
childB.join()
}
}
fun supervisorScope() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
// some print, analytics, etc
}
supervisorScope {
val childWithOwnHandler = launch(exceptionHandler) {
// child has own exception handler
delay(10)
throw NullPointerException()
}
delay(25)
// some work here still possible
// only childWithOwnHandler was cancelled
}
}

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store