Kotlin Coroutines Tips & Tricks

Ing. Jan Kaláb, MasterAPP

Kotlin Coroutines 101

Kotlin

  • JVM/Java kompatibilní programovací jazyk.
  • JavaScript, native
  • Java na steroidech!

Coroutines

  • Způsob, jak psát paralelní kód
  • Kotlin 1.3.0
  • Lehká vlákna
  • Kooperativní multitasking
  • launch, async/await, …

suspend fun answer() = async {
  delay(1000) // Heavy calculation, seriously
  42
}

suspend fun main() {
	println(answer().await())
}
						

Suspend main


fun main(/*args: Array<String>*/) = runBlocking {
}

suspend fun main(/*args: Array<String>*/) {
}
					

Suspend fun vs. builder

Suspend fun dokud to jde. Až to nejde (override fun), tak builder.

Nebo když jste si opravdu jistí, že se má něco provádět paralelně.

Nebo když prostě víte co děláte…

Dispatchers

Všechno Default (CPU), co nejméně si odskakovat do Main (UI) a IO.


launch(/*Dispatchers.Default*/) {
  val data = withContext(Dispatchers.IO) {
    // Read something from disc or internet
  }
  val processed = process(data) // Back on Default dispatcher
  withContext(Dispatchers.Main) {
    // Show result in UI
  }
}
					
Default
počet jader (nejméně 2)
IO
počet jader (nejméně 64)


  • Default a IO sdílejí pool, nedochází ke context switchům.
  • Knihovny (OkHttp) často mají vlastní pooly, netřeba používat IO
  • SharedPreferences při čtení sahá na disk! (~ 80 ms)

Scope

Cokoliv má životní cyklus (Closeable), mělo by mít vlastní scope.


class Foo :
    Closeable,
    CoroutineScope by CoroutineScope(Dispatchers.Default) {
  override fun close() {
    cancel() // Scope
  }
}
						

GlobalScope = ZLO!

MainScope

Běží na main vlákně!


class MyActivity : Activity(), CoroutineScope by MainScope() {
  override fun onDestroy() {
    super.onDestroy()
    cancel() // Scope
  }
}
						

class MyFragment : Fragment(), CoroutineScope by MainScope() {
  override fun onDetach() {
    super.onDetach()
    cancel() // Scope
  }
}
						

LifecycleScope

Běží na main vlákně!


dependencies {
  implementation 'androidx.lifecycle:lifecycle-runtime-ktx:+'
}
						

class MyFragment : Fragment() { // Activity, Service
  fun foo() {
    // Attach/detach
    lifecycleScope.launch {}

    // viewCreated/destroyed
    viewLifecycleOwner.lifecycleScope.launch {} 
  }
}
						

ViewModelScope


dependencies {
  implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:+'
}
						

class MyViewModel : ViewModel() {
  fun foo() {
    viewModelScope.launch {
      // Main
    }
  }
}
					

Jobs, cancellation

Exception bubbling


launch { // ①
  launch { // ②
    // Stuff
  }

  launch { // ③
    throw Exception()
  }
}
						
  1. Výjimka z ③ probublá do rodiče ①.
  2. ① nemá žádného rodiče → cancel.
  3. ② se ukončí (CancellationException).

async si drží výjimku až do await().


try {
  launch {
  }
}

launch {
  try {
  }
}
						

Cancellation propagation


val job = launch {
  launch {
    // Stuff
  }
  launch {
    // Stuff
  }
}

job.cancel()
//job.cancelChildren()

launch(job) {
  // Nope!
}
						
  • Cancel rodiče ukončí i jeho potomky.
  • Zrušený Job už nemůže mít žádné potomky!

SupervisorJob

Výjimka v potomkovi neukončí rodiče.


val supervisor = SupervisorJob()

launch(supervisor) {
  // Stuff
}

launch(supervisor) {
  throw Exception() // Everything is fine.
}
						

Možnost vlastního zpracování výjimek.

Endless loop


launch {
  while(true) {
    // Endless, ignores cancellation!
  }
}

launch {
  while(isActive) {
    // Ends with cancellation.
  }
}
						

Multiple parents


val job1 = Job()
val job2 = Job()

launch(job1 + job2) {
  // Stuff
}

job2.cancel()
job1.isActive // true
						

cancel jednoho z rodiču zruší i závislé joby.

Callbacks


suspend fun OkHttpClient.coEnqueue(request: Request) =
  suspendCancellableCoroutine<Response> { cont ->
    newCall(request).apply {
      cont.invokeOnCancellation { cancel() }
      enqueue(object : Callback {
        override fun onResponse(call: Call, response: Response) =
          cont.resume(response)
        override fun onFailure(call: Call, e: IOException) =
          cont.resumeWithException(e)
      })
    }
  }
					

val data = try {
  http.coEnqueue(request).body.string()
} catch (ioe: IOException) {
  null
}
					

Actor

Channel s logikou

  • Click spamming
  • Sensors processing

val actor = actor<Int>(capacity = Channel.CONFLATED) {
  consumeEach {
    delay(1000)
    println(it)
  }
}

actor.offer(1)
actor.offer(2)
actor.offer(3)
						

1
3
						

Debugging


import kotlinx.coroutines.*

class App : Application() {
  init {
    System.setProperty(
      DEBUG_PROPERTY_NAME,
      DEBUG_PROPERTY_VALUE_ON
    )
  }
}
					

launch(Dispatchers.Main + CoroutineName("foobar")) {
  println("${Thread.currentThread().name}\tHello")
}
					
main @foobar#8	Hello

Knihovny

Retrofit 2.6


interface StuffController {
  @GET("stuff")
  suspend fun getStuff(): Stuff
}
						

Room 2.1

implementation 'androidx.room:room-coroutines:2.+'

@Dao
interface UsersDao {
  @Query("SELECT * FROM users")
  suspend fun getUsers(): List<User>
}
						

MockK


coEvery { foo.bar() } returns 42
coVerify { foo.bar() }
						

Shrnutí

  • Pužívejte vhodné dispatchery ve scopech.
  • Pozor na výjimky a cancel() v launch.
  • Přepište jednoduché callbacky na suspend funkce.
  • Vyzkoušejte actor na UI události.
  • Zapněte si debugging.