在 Android 中使用協程(Coroutine)

clipboard.png

簡評:可能對於不少的 Android程序員來講協程(Coroutine)並非一個熟悉的概念,更可能是和線程、回調打交道。但協程這一律念其實很早就提出來了,C#, Lua, Go 等語言也支持協程,Kotlin 也提供了 kotlinx.coroutines 庫來幫助使用協程。因此,今天這裏就介紹下怎麼經過 Kotlin 在 Android 中使用協程。html

Coroutine 中文大多翻譯爲「協程」,相關概念網上已有不少相關的資料(《計算機程序設計藝術 卷一》中就有講到 Coroutine),這裏就再也不贅述。java

在這篇文章中,主要關注如何經過 kotlinx.coroutines 庫來在 Android 中實現 Coroutine。android

如何啓動一個協程(Coroutine)git

在 kotlinx.coroutines 庫中,咱們能夠使用 launchasync 來啓動一個新的 coroutine。程序員

從概念上講,async 和 launch 是相似的,區別在於 launch 會返回一個 Job 對象,不會攜帶任何結果值。而 async 則是返回一個 Deferred - 一個輕量級、非阻塞的 future,表明了以後將會提供結果值的承諾(promise),所以能夠使用 .await() 來得到其最終的結果,固然 Deferred 也是一個 Job,若是須要也是能夠取消的。github

若是你對於 future, promise, deferred 等概念感到困惑,能夠先閱讀併發 Promise 模型或其餘資料瞭解相關概念。segmentfault

Coroutine contextapi

在 Android 中咱們常常使用兩類 context:promise

  • uiContext: 用於執行 UI 相關操做。
  • bgContext: 用於執行須要在後臺運行的耗時操做。
// dispatches execution onto the Android main UI thread
private val uiContext: CoroutineContext = UI
 
// represents a common pool of shared threads as the coroutine dispatcher
private val bgContext: CoroutineContext = CommonPool

這裏 bgContext 使用 CommonPool,能夠限制同時運行的線程數小於 Runtime.getRuntime.availableProcessors() - 1。併發

launch + async (execute task)

父協程(The parent coroutine)使用 uiContext 經過 launch 啓動。

子協程(The child coroutine)使用 CommonPool context 經過 async 啓動。

注意:

  1. 父協程老是會等待全部的子協程執行完畢。
  2. 若是發生未檢查的異常,應用將會崩潰。

下面實現一個簡單的讀取數據並由視圖進行展現的例子:

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until finished
 
    view.showData(result) // ui thread
}

launch + async + async (順序執行兩個任務)

下面的兩個任務是順序執行的:

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    // non ui thread, suspend until task is finished
    val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await()
 
    // non ui thread, suspend until task is finished
    val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await()
 
    val result = "$result1 $result2" // ui thread
 
    view.showData(result) // ui thread
}

launch + async + async (同時執行兩個任務)

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    val task1 = async(bgContext) { dataProvider.loadData("Task 1") }
    val task2 = async(bgContext) { dataProvider.loadData("Task 2") }
 
    val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished
 
    view.showData(result) // ui thread
}

啓動 coroutine 並設置超時時間

能夠經過 withTimeoutOrNull 來給 coroutine job 設置時限,若是超時將會返回 null。

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
 
    // non ui thread, suspend until the task is finished or return null in 2 sec
    val result = withTimeoutOrNull(2, TimeUnit.SECONDS) { task.await() }
 
    view.showData(result) // ui thread
}

如何取消一個協程(coroutine)

var job: Job? = null
 
fun startPresenting() {
    job = loadData()
}
 
fun stopPresenting() {
    job?.cancel()
}
 
private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until finished
 
    view.showData(result) // ui thread
}

當父協程被取消時,全部的子協程將遞歸的被取消。

在上面的例子中,若是 stopPresenting 在被調用時 dataProvider.loadData 正在運行,那麼 view.showData 方法將不會被調用。

如何處理異常
try-catch block

咱們仍是能夠像平時同樣用 try-catch 來捕獲和處理異常。不過這裏推薦將 try-catch 移到 dataProvider.loadData 方法裏面,而不是直接包裹在外面,並提供一個統一的 Result 類方便處理。

data class Result<out T>(val success: T? = null, val error: Throwable? = null)
 
private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result: Result<String> = task.await() // non ui thread, suspend until the task is finished
 
    if (result.success != null) {
        view.showData(result.success) // ui thread
    } else if (result.error != null) {
        result.error.printStackTrace()
    }
}

async + async

當經過 async 來啓動父協程時,將會忽略掉任何異常:

private fun loadData() = async(uiContext) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until the task is finished
 
    view.showData(result) // ui thread
}

在這裏 loadData() 方法會返回 Job 對象,而 exception 會被存放在這個 Job 對象中,咱們能夠用 invokeOnCompletion 函數來進行檢索:

var job: Job? = null
 
fun startPresenting() {
    job = loadData()
    job?.invokeOnCompletion { it: Throwable? ->
        it?.printStackTrace() // (1)
        // or
        job?.getCompletionException()?.printStackTrace() // (2)
 
 
        // difference between (1) and (2) is that (1) will NOT contain CancellationException
        // in case if job was cancelled
    }
}

launch + coroutine exception handler

咱們還能夠爲父協程的 context 中添加 CoroutineExceptionHandler 來捕獲和處理異常:

val exceptionHandler: CoroutineContext = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() }
 
private fun loadData() = launch(uiContext + exceptionHandler) {
    view.showLoading() // ui thread
 
    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until the task is finished
 
    view.showData(result) // ui thread
}

怎麼測試協程(coroutine)?

要啓動協程(coroutine)必需要指定一個 CoroutineContext

class MainPresenter(private val view: MainView,
                    private val dataProvider: DataProviderAPI) {
 
    private fun loadData() = launch(UI) { // UI - dispatches execution onto Android main UI thread
        view.showLoading()
 
        // CommonPool - represents common pool of shared threads as coroutine dispatcher
        val task = async(CommonPool) { dataProvider.loadData("Task") }
        val result = task.await()
 
        view.showData(result)
    }
 
}

所以,若是你想要爲你的 MainPresenter 寫單元測試,你就必需要能爲 UI 和 後臺任務指定 coroutine context。

最簡單的方式就是爲 MainPresenter 的構造方法增長兩個參數,並設置默認值:

class MainPresenter(private val view: MainView,
                    private val dataProvider: DataProviderAPI
                    private val uiContext: CoroutineContext = UI,
                    private val ioContext: CoroutineContext = CommonPool) {
 
    private fun loadData() = launch(uiContext) { // use the provided uiContext (UI)
        view.showLoading()
 
        // use the provided ioContext (CommonPool)
        val task = async(bgContext) { dataProvider.loadData("Task") }
        val result = task.await()
 
        view.showData(result)
    }
 
}

如今,就能夠在測試中傳入 kotlin.coroutines 提供的 EmptyCoroutineContext 來讓代碼運行在當前線程裏。

@Test
fun test() {
    val dataProvider = Mockito.mock(DataProviderAPI::class.java)
    val mockView = Mockito.mock(MainView::class.java)
 
    val presenter = MainPresenter(mockView, dataProvider, EmptyCoroutineContext, EmptyCoroutineContext)
    presenter.startPresenting()
    ...
}

上面就是 kotlin 中協程(coroutine)的基本用法,完整代碼能夠查看 Github 項目。

若是想了解更多內容還能夠查看 Kotlin 的官方示例:Kotlin/kotlinx.coroutines

原文:Android Coroutine Recipes

相關文章
相關標籤/搜索