Kotlin Coroutines Flow 系列(三) 異常處理 五. Flow 異常處理 六. Flow Lifecycle

五. Flow 異常處理

Flow 可以使用傳統的 try...catch 來捕獲異常:

fun main() = runBlocking {
    flow {
        emit(1)
        try {
            throw RuntimeException()
        } catch (e: Exception) {
            e.stackTrace
        }

    }.onCompletion { println("Done") }
        .collect { println(it) }
}

另外,也可以使用 catch 操作符來捕獲異常。

5.1 catch 操作符

上一篇文章Flow VS RxJava2曾講述過 onCompletion 操作符。

但是 onCompletion 不能捕獲異常,只能用於判斷是否有異常。

fun main() = runBlocking {
    flow {
        emit(1)
        throw RuntimeException()
    }.onCompletion { cause ->
        if (cause != null)
            println("Flow completed exceptionally")
        else
            println("Done")
    }.collect { println(it) }
}

執行結果:

1
Flow completed exceptionally
Exception in thread "main" java.lang.RuntimeException
......

catch 操作符可以捕獲來自上游的異常

fun main() = runBlocking {
    flow {
        emit(1)
        throw RuntimeException()
    }
    .onCompletion { cause ->
        if (cause != null)
            println("Flow completed exceptionally")
        else
            println("Done")
    }
    .catch{ println("catch exception") }
    .collect { println(it) }
}

執行結果:

1
Flow completed exceptionally
catch exception

上面的代碼如果把 onCompletion、catch 交換一下位置,則 catch 操作符捕獲到異常後,不會影響到下游。因此,onCompletion 操作符不再打印"Flow completed exceptionally"

fun main() = runBlocking {
    flow {
        emit(1)
        throw RuntimeException()
    }
    .catch{ println("catch exception") }
    .onCompletion { cause ->
        if (cause != null)
            println("Flow completed exceptionally")
        else
            println("Done")
    }
    .collect { println(it) }
}

執行結果:

1
catch exception
Done

catch 操作符用於實現異常透明化處理。例如在 catch 操作符內,可以使用 throw 再次拋出異常、可以使用 emit() 轉換爲發射值、可以用於打印或者其他業務邏輯的處理等等。

但是,catch 只是中間操作符不能捕獲下游的異常,類似 collect 內的異常。

對於下游的異常,可以多次使用 catch 操作符來解決。

對於 collect 內的異常,除了傳統的 try...catch 之外,還可以藉助 onEach 操作符。把業務邏輯放到 onEach 操作符內,在 onEach 之後是 catch 操作符,最後是 collect()。

fun main() = runBlocking<Unit> {
    flow {
         ......
    }
    .onEach {
          ......
    }
   .catch { ... }
   .collect()
}

5.2 retry、retryWhen 操作符

像 RxJava 一樣,Flow 也有重試的操作符。

如果上游遇到了異常,並使用了 retry 操作符,則 retry 會讓 Flow 最多重試 retries 指定的次數。

public fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
    require(retries > 0) { "Expected positive amount of retries, but had $retries" }
    return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}

例如,下面打印了三次"Emitting 1"、"Emitting 2",最後兩次是通過 retry 操作符打印出來的。

fun main() = runBlocking {

    (1..5).asFlow().onEach {
        if (it == 3) throw RuntimeException("Error on $it")
    }.retry(2) {

        if (it is RuntimeException) {
            return@retry true
        }
        false
    }
    .onEach { println("Emitting $it") }
    .catch { it.printStackTrace() }
    .collect()
}

執行結果:

Emitting 1
Emitting 2
Emitting 1
Emitting 2
Emitting 1
Emitting 2
java.lang.RuntimeException: Error on 3
......

retry 操作符最終調用的是 retryWhen 操作符。下面的代碼跟剛纔的執行結果一致:

fun main() = runBlocking {

    (1..5).asFlow().onEach {
        if (it == 3) throw RuntimeException("Error on $it")
    }
    .onEach { println("Emitting $it") }
    .retryWhen { cause, attempt ->
        attempt < 2
    }
    .catch { it.printStackTrace() }
    .collect()
}

因爲 retryWhen 操作符的參數是謂詞,當謂詞返回 true 時纔會進行重試。謂詞還接收一個 attempt 作爲參數表示嘗試的次數,該次數是從0開始的。

六. Flow Lifecycle

RxJava 的 do 操作符能夠監聽 Observables 的生命週期的各個階段。

Flow 並沒有多那麼豐富的操作符來監聽其生命週期的各個階段,目前只有 onStart、onCompletion 來監聽 Flow 的創建和結束。

fun main() = runBlocking {

    (1..5).asFlow().onEach {
        if (it == 3) throw RuntimeException("Error on $it")
    }
    .onStart { println("Starting flow") }
    .onEach { println("On each $it") }
    .catch { println("Exception : ${it.message}") }
    .onCompletion { println("Flow completed") }
    .collect()
}

執行結果:

Starting flow
On each 1
On each 2
Flow completed
Exception : Error on 3

例舉他們的使用場景:
比如,在 Android 開發中使用 Flow 創建網絡請求時,通過 onStart 操作符調用 loading 動畫以及網絡請求結束後通過 onCompletion 操作符取消動畫。

再比如,在藉助這些操作符做一些日誌的打印。

fun <T> Flow<T>.log(opName: String) = onStart {
    println("Loading $opName")
}.onEach {
    println("Loaded $opName : $it")
}.onCompletion { maybeErr ->
    maybeErr?.let {
        println("Error $opName: $it")
    } ?: println("Completed $opName")
}

該系列的相關文章:

Kotlin Coroutines Flow 系列(一) Flow 基本使用
Kotlin Coroutines Flow 系列(二) Flow VS RxJava2
Kotlin Coroutines Flow 系列(四) 線程操作
Kotlin Coroutines Flow 系列(五) 其他的操作符

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章