Android Coroutine Recipes

本文的原始碼可以在YangQiRen/coroutine-recipes中找到

基於Coroutine v1.0.0版本

引入依賴

implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation 
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'

如何啟動協程(Coroutine)

kotlinx.coroutines 庫中,您可以使用 launch async 函數來啟動新的協程。

從概念上講, async 就像 launch 。它啟動一個單獨的協程,這是一個輕量級線程,可與所有其他協程同時工作。區別在於, launch 返回一個 Job 且不攜帶任何結果值,而 async 返回一個 Deferred 輕量無阻塞的未來,表示希望稍後提供結果。您可以使用 .await() 延期的值來獲得其最終結果,但是 Deferred 它也是一個 Job ,因此可以根據需要取消它。

***重點***

如果裡面的代碼 launch 以異常終止,然後將其視為未捕獲線程中的異常會使Android應用程序崩潰。內部未捕獲的異常 async 代碼存儲在結果中 Deferred 並且不會在其他任何地方交付,除非經過處理,否則它將被靜默丟棄。

協程調度員(Coroutine Dispatcher)

在Android中,我們通常使用兩個調度程序:
  • uiDispatcher將執行分派到Android主UI線程(用於父協程)。
  • bgDispatcher在後台線程中分派執行(用於子協程)。
//將執行分派到Android主線程中
val uiDispatcher:CoroutineDispatcher  = Dispatchers.Main 
//將共享線程池表示為協程分派器
val bgDispatcher:CoroutineDispatcher =  Dispatchers.I0
在下面的例子中,我們將使用 CommonPool 用於 bgContext 這限制並行運行為值64個線程或核的數量的線程的數目。

您可能要考慮使用 newFixedThreadPoolContext 緩存線程池或使用自己的緩存線程池實現。

協程範圍

要啟動協程,您需要提供 CoroutineScope 或使用 GlobalScope
// GlobalScope example
class MainFragment : Fragment() {
    fun loadData() = GlobalScope.launch {  ...  }
}
// CoroutineScope example
class MainFragment : Fragment() {

    val uiScope = CoroutineScope(Dispatchers.Main)

    fun loadData() = uiScope.launch {  ...  }
}
// Fragment implements CoroutineScope example
class MainFragment : Fragment(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main

    fun loadData() = launch {  ...  }
}

launch + async (execute task)

父協程通過分派器的 launch 功能啟動 Main

子協程通過調度程序的 async 功能啟動 IO

注意:父協程始終等待其所有子進程完成。

注意:如果發生未經檢查的異常,應用程序將崩潰。
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread
    val task = async(bgDispatcher) { // background thread
        // your blocking call
    }
    val result = task.await()
    view.showData(result) // ui thread
}

launch + withContext (execute task)

儘管前面的示例運行良好,但我們通過啟動第二個協程來進行後台工作來浪費資源。如果僅啟動一個協程並用於 withContext 切換協程上下文,則可以優化代碼。

父協程通過分派器的 launch 功能啟動 Main

背景作業通過執行 withContext 與功能的 IO 調度。
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread
    val result = withContext(bgDispatcher) { // background thread
        // your blocking call
    }
    view.showData(result) // ui thread
}

launch + withContext (execute two tasks sequentially)

父協程通過分派器的 launch 功能啟動 Main

背景作業通過執行 withContext 與功能的 IO 調度。

注意: result1 result2 順序執行。

注意:如果發生未經檢查的異常,應用程序將崩潰。
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread

    val result1 = withContext(bgDispatcher) { // background thread
        // your blocking call
    }

    val result2 = withContext(bgDispatcher) { // background thread
        // your blocking call
    }
    
    val result = result1 + result2
    
    view.showData(result) // ui thread
}

launch + async + async (execute two tasks parallel)

父協程通過分派器的 launch 功能啟動 Main

背景作業通過執行 async 與功能的 IO 調度。

注意: task1 task2 是並行執行。

注意:如果發生未經檢查的異常,應用程序將崩潰。
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread

    val task1 = async(bgDispatcher) { // background thread
        // your blocking call
    }

    val task2 = async(bgDispatcher) { // background thread
        // your blocking call
    }

    val result = task1.await() + task2.await()

    view.showData(result) // ui thread
}

How to launch a coroutine with a timeout

如果要為協程作業設置超時,請使用暫停的函數包裝該函數 withTimeoutOrNull ,如果超時,該函數將返回 null
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread
    val task = async(bgDispatcher) { // background thread
        // your blocking call
    }
    // suspend until task is finished or return null in 2 sec
    val result = withTimeoutOrNull(2000) { task.await() }
    view.showData(result) // ui thread
}

How to cancel a coroutine

job

該函數 loadData 返回一個 Job 可能被取消的對象。當父協程被取消時,其所有子進程也將被遞歸取消。

如果 stopPresenting 函數被調用,同時 dataProvider.loadData 仍在進行中,該功能 view.showData 將永遠不會被調用。
val uiScope = CoroutineScope(Dispatchers.Main)
var job: Job? = null
fun startPresenting() {
    job = loadData()
}
fun stopPresenting() {
    job?.cancel()
}

fun loadData() = uiScope.launch {
    view.showLoading() // ui thread
    val result = withContext(bgDispatcher) { // background thread
        // your blocking call
    }
    view.showData(result) // ui thread
}

parent job

取消協程的另一種方法是創建SupervisorJob對象,並通過重載的+運算符在作用域構造函數中指定它。在此處閱讀有關結合協程上下文元素的更多信息
注意:如果取消父作業,則需要創建新的Job對象才能啟動新的協程,這就是我們使用cancelChildren的原因。
var job = SupervisorJob()
val uiScope = CoroutineScope(Dispatchers.Main + job)
fun startPresenting() {
    loadData()
}
fun stopPresenting() {
    scope.coroutineContext.cancelChildren()
}
fun loadData() = uiScope.launch {
    view.showLoading() // ui thread
    val result = withContext(bgDispatcher) { // background thread
        // your blocking call
    }
    view.showData(result) // ui thread
}

lifecycle aware coroutine scope

隨著android體系結構組件的發布,我們可以創建生命週期感知的協程範圍,該範圍將在 Activity#onDestroy 事件發生時自行取消。

用於生命週期的協程範圍的示例 LifecycleObserver
class MainScope : CoroutineScope, LifecycleObserver {

    private val job = SupervisorJob()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun destroy() = coroutineContext.cancelChildren()
}
// usage
class MainFragment : Fragment() {
    private val uiScope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycle.addObserver(mainScope)
    }

    private fun loadData() = uiScope.launch {
        val result = withContext(bgDispatcher) {
            // your blocking call
        }
    }
}

How to handle exceptions with coroutines

try-catch block

父協程通過分派器的 launch 功能啟動 Main

子協程通過調度程序的 async 功能啟動 IO

注意:您可以使用 try-catch 塊來捕獲異常並進行處理。

留言

這個網誌中的熱門文章

Android - 使用 adb 安装apk

Android ContentProvider 實現多個應用程式共享資料

Android TextView autosizing 自動調整大小