Help us understand the problem. What is going on with this article?

Kotlin Coroutineの悪い例と解決策

More than 1 year has passed since last update.

昨日、Coroutineを理解するために以下の記事を書いた

Kotlin Coroutinesパターン&アンチパターン - Qiita
https://qiita.com/ikemura23/items/fb8caeba4c35fcd85644

ただ書いただけではなく、本当にそうなのか実装して確かめてみた。

例外処理をtry/catchに頼らない

エラーが発生するdoWork()をtry / catchしてもダメ

class MainActivity : AppCompatActivity() {

    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    override fun onResume() {
        super.onResume()

        // Throw Exceptionの可能性がある処理
        fun doWork(): Deferred<String> = scope.async { throw UnknownError() }

        scope.launch {
            try {
                doWork().await() // ここで落ちる
            } catch (e: Exception) {
                Log.d(TAG, "ここに到達しない")
            }
        }
    }

結果

実行すると、trt / catchの意味なくエラーでアプリが落ちる

E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: com.ikemura.android_kotlin_lab, PID: 9287
    java.util.concurrent.TimeoutException
        ...

解決策

SupervisorJobで子のエラーを回避する。

class MainActivity : AppCompatActivity() {

    private val job = SupervisorJob() // <= SupervisorJobに変更
    private val scope = CoroutineScope(Dispatchers.Default + job)

    override fun onResume() {
        super.onResume()

        // Throw Exceptionの可能性がある処理
        fun doWork(): Deferred<String> = scope.async { throw UnknownError() }

        scope.launch {
            try {
                doWork().await() // ここで落ちる
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }

注意すべきasync

↑で示した解決は、SupervisorJobを使用してCoroutineScopeで非同期を明示的に実行した場合にのみ機能する。

以下の async はSupervisorJobを使用してもエラーとなる。

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() = scope.launch {
        try {
            async {
                doWork() // ここで落ちる
            }
        } catch (e: Exception) {
            Log.d(TAG, "キャッチされない")
        }
    }

理由は、async内で別のcoroutineが作成され、その範囲内で起動されるから。(ちょっと曖昧)

解決策

coroutineScopeを使ってasyncをラップする。

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {

        scope.launch {
            try {
                coroutineScope { // 追加
                    async {
                        doWork()
                    }
                }
                Log.d(TAG, "ここは実行されない")
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }

上記コードは分かりにくいので、こう見やすくできる。

    // エラーの可能性がある処理
    suspend fun doWork(): Deferred<String> = coroutineScope {
        async { throw TimeoutException() }
    }

    fun loadData() {
        scope.launch {
            try {
                doWork() // coroutineScopeとasyncをdoWorkに移動させた
                Log.d(TAG, "ここは実行されない")
            } catch (e: Exception) {
                Log.d(TAG, "キャッチされる")
            }
        }
    }

try/catchばかり使わない

SupervisorJobを作り、try/catchでラップしておけば大概のエラーはキャッチできるはず。
だが毎回try/catchを使うのではなく、コルーチンの例外をキャッチしてくれるCoroutineExceptionHandlerが用意されているので、こちらを使う。

CoroutineExceptionHandlerを個別に指定する例

    // エラーハンドリングを作成
    private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e(TAG, "例外キャッチ $throwable") // ログ出力のみ
    }
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {
        scope.launch(exceptionHandler) { // エラーハンドリングを指定
            doWork()
        }
    }

ログ

E/MainActivity: 例外キャッチ java.util.concurrent.TimeoutException

try/catchがなくなり、スッキリした。

CoroutineExceptionHandlerを共通で使い回す

CoroutineExceptionHandlerを個別に指定するのもいいが、CoroutineScopeに渡せば共通で使い回せる。

private val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e(TAG, "例外キャッチ $throwable")
    }
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job + exceptionHandler) // exceptionHandlerを渡す

    // エラーの可能性がある処理
    fun doWork(): Deferred<String> = throw TimeoutException()

    fun loadData() {
        scope.launch { // exceptionHandler指定が無くなった
            doWork()
            Log.d(TAG, "到達しない")
        }
    }

普段は共通でしておき、必要な時だけ個別に例外を指定するのがベストだろう。

実装したコード

https://github.com/ikemura23/Android-Kotlin-Lab/tree/coroutine-sample

参考リンク

https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e

ikemura23
Androidアプリエンジニアです。 マイブームはFlutterとAWS
http://banbara-studio.hatenablog.com/
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc
http://www.yumemi.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away