Search code examples
kotlinfileiokotlin-coroutines

What's the proper way of returning a result out of a IO coroutine job?


The problem is very simple, but I can't really seem to wrap my head around it. I'm launching a non-blocking thread in the IO scope in order to read from a file. However, I can't get the result in time before I return from the method - it always returns the initial empty value "". What am I missing here?

private fun getFileContents(): String {
        var result = ""
        val fileName = getFilename()
        val job = CoroutineScope(Dispatchers.IO).launch {
            kotlin.runCatching {
                val file = getFile(fileName)
                file.openFileInput().use { inputStream ->
                    result = String(inputStream.readBytes(), Charsets.UTF_8)
                }
            }
        }
        return result
    }

Solution

  • Coroutines are launched asynchronously. Your non-suspending function cannot wait for the result without blocking. For more information about why asynchronous code results in your function returning with the default result, read the answers here.

    getFileContents() has to be a suspend function to be able to return something without blocking, in which case you don't need to launch a coroutine either. But then whatever calls this function must be in a suspend function or coroutine.

    private suspend fun getFileContents(): String = withContext(Dispatchers.IO) {
        val fileName = getFilename()
        kotlin.runCatching {
            val file = getFile(fileName)
            file.openFileInput().use { inputStream ->
                result = String(inputStream.readBytes(), Charsets.UTF_8)
            }
        }.getOrDefault("")
    }
    

    There are two "worlds" of code: either you are in a suspending/coroutine context or you are not. When you are in a function that is not a suspend function, you can only return results that can be computed immediately, or you can block until the result is ready.

    Generally, if you're using coroutines, you launch a coroutine at some high level in your code, and then you are free to use suspend functions everywhere because almost all of your code is initially triggered by a coroutine. By "high level", I mean you launch the coroutine when a UI screen appears or a UI button is pressed, for example.

    Basically, your coroutine launches are usually in UI listeners and UI event functions, not in lower-level code like the function in your question. The coroutine calls a suspend function, which can call other suspend functions, so you don't need to launch more coroutines to perform your various sequential tasks.


    The alternate solution is to return a Deferred with the result, like this:

    private fun getFileContents(): Deferred<String> {
        val fileName = getFilename()
        return CoroutineScope(Dispatchers.IO).async {
            kotlin.runCatching {
                val file = getFile(fileName)
                file.openFileInput().use { inputStream ->
                    result = String(inputStream.readBytes(), Charsets.UTF_8)
                }
            }.getOrDefault("")
        }
    }
    

    But to unpack the result, you will need to call await() on the Deferred instance inside a coroutine somewhere.