does IO in coroutines cause suspension?

There's no automagic going on with Kotlin coroutines. If you call a blocking function like HttpReader.get(), the coroutine won't be suspended and instead the call will block. You can easily assure yourself that a given function won't cause the coroutine to suspend: if it's not a suspend function, it cannot possibly do it, whether or not it's called from a suspend function.

If you want to turn an existing blocking API into non-blocking, suspendable calls, you must submit the blocking calls to a threadpool. The easiest way to achieve it is as follows:

val response = withContext(Dispatchers.IO) { HttpReader.get(url) }

withContext is a suspend fun that will suspend the coroutine, submit the provided block to another coroutine dispatcher (here IO) and resume when that block is done and has come up with its result.

You can also easily instantiate your own ExecutorService and use it as a coroutine dispatcher:

val myPool = Executors.newCachedThreadPool().asCoroutineDispatcher()

Now you can write

val response = withContext(myPool) { HttpReader.get(url) }

This PR has example code for proper OkHttp coroutines support

https://github.com/square/okhttp/pull/4129/files

It uses the thread pools of OkHttp to do the work. The key bit of code is this generic library code.

 suspend fun OkHttpClient.execute(request: Request): Response {
   val call = this.newCall(request)
   return call.await()
 }

 suspend fun Call.await(): Response {
  return suspendCancellableCoroutine { cont ->
    cont.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback {
      override fun onFailure(call: Call, e: IOException) {
        if (!cont.isCancelled) {
          cont.resumeWithException(e)
        }
      }
       override fun onResponse(call: Call, response: Response) {
        if (!cont.isCancelled) {
          cont.resume(response)
        }
      }
    })
  }
}