Kotlinコルーチンがとても便利だという話

今回は、Kotlinのコルーチンという機能についてお話したいと思います。

まずはじめに、Androidアプリにおいて画面表示に関わる処理は、UIスレッドでしか実行できません。テキストの内容を書き換えたり、画像を表示したりといった処理は全てUIスレッドで実行する必要があります。
一方で、何秒もかかるような重い処理をUIスレッドで実行してしまうと、アプリや端末がフリーズしているように見えてしまいます。
これはUIスレッドがアプリごとに1つしか存在しないために起こります。重い処理がUIスレッドを専有してしまうと、他の処理がUIスレッドを利用できなくなり、画面が固まってしまいます。
重い処理の代表例としてはネットワーク通信やファイル操作などが挙げられます。これらの処理は、そもそもUIスレッドでの実行が禁止や非推奨とされているケースも多いです。

ここで、例として次のような処理を考えてみます。
・「Now loading」を表示
・APIリクエストしてサーバーからデータを取得
・取得したデータを画面に表示
・「Now loading」の表示を消す

Now loadingやデータ表示などはUIスレッドでしか実行できません。逆に、APIリクエストはUIスレッドでは実行できないとします。

こういった場合、マルチスレッド処理によってUIスレッド以外のスレッドでAPIリクエストを処理するのですが、よくあるパターンとしてコールバックパターンがあるかと思います。
例えばコールバックパターンで上記を実現する場合、以下のような実装になると思います。

//
// 「Now loading」を表示
//
showLoadingProgress()

//
// マルチスレッドでAPIリクエストし、結果を取得する
//
fetchData(apiUrl) { result ->
    //
    // APIの結果を画面に表示
    //
    showResult(result)

    //
    // 「Now loading」の表示を消す
    //
    hideLoadingProgress()
}

この程度なら読みづらいという程ではないと思いますが、例えば次のように、エラーに備えてtry catchを組み合わせる等すると、一気に読みづらいコードになってしまいます。

try {
    //
    // 「Now loading」を表示
    //
    showLoadingProgress()

    //
    // マルチスレッドでAPIリクエストし、結果を取得する
    //
    fetchData(apiUrl) { result ->

        // コールバック内で発生したエラーは、外側のtry catchではキャッチできない!

        //
        // APIの結果を画面に表示
        //
        showResult(result)

        //
        // 「Now loading」の表示を消す
        //
        hideLoadingProgress()
    }
} catch (e: Throwable) {
    //
    // エラー処理
    //
} finally {
    //
    // APIリクエストの完了前に実行されてしまうため、ここで「Now loading」を消す処理は実行できない
    //
    // hideLoadingProgress()
}

上記のような場合、プログラムの処理順が下から上にさかのぼったりと複雑になってしまいます。また、コールバックの中で発生したエラーはこのtry catchではキャッチできません。

このように、コールバックにはプログラムが複雑になったり機能や書き方が制限される欠点があります。このような場合、Kotlin コルーチンを利用することで、シンプルで読みやすいコードに書き換えることができます。
Kotlin コルーチンを使えば、上記コードは以下のように書き換えられます。

try {
    withContext(Dispatchers.Main) {
        //
        // 「Now loading」を表示
        //
        showLoadingProgress()
    }

    val result = withContext(Dispatchers.IO) {
        //
        // バックグラウンドスレッドでAPIリクエストし、結果を取得する
        //
        fetchData(apiUrl)
    }

    withContext(Dispatchers.Main) {
        //
        // APIの結果を画面に表示
        // ここで発生したエラーはtry catchでキャッチできる
        //
        showResult(result)
    }

} catch (e: Throwable) {
    //
    // エラー処理
    //
} finally {
    //
    // withContextの処理が完了するまで待ってくれるので、ここで「Now loading」の表示を消す事ができる
    //
    withContext(Dispatchers.Main) {
        hideLoadingProgress()
    }
}

withContextという記述が追加されていますが、これがKotlinコルーチンの機能の一つです。これは指定したスレッドで処理を実行し、その完了を待つことができる機能です。Mainを指定すればUIスレッドで、IOを指定すれば重い処理に適したバックグラウンドスレッドで処理ができるため、APIやファイル操作を含む複雑な処理フローでもシンプルに記述できます。さらに、withContextが自動で待ってくれることで、上から下に処理が実行されることが保証されます。

これにより、コールバックのような複雑なフローを避けることができます。また、処理全体を一つのtryブロックで囲み、APIリクエストの後に行う処理で発生したエラーもキャッチすることができます。

マルチスレッド処理の実装は複雑になりがちなものですが、Kotlinコルーチンによってとてもシンプルに記述できると分かりました。より分かりやすいプログラムのために、Kotlinコルーチンを活用していきたいと思います。