LoginSignup
56
36

More than 3 years have passed since last update.

Kotlin Coroutineの悪い例と解決策

Last updated at Posted at 2019-02-14

昨日、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, "到達しない")
        }
    }

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

実装したコード

参考リンク

56
36
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
36