Android 上的 Kotlin 協同程式

「協同程式」是可在 Android 上使用的並行設計模式,用於簡化非同步執行的程式碼。根據其他語言的既有概念,Kotlin 在 1.3 版中新增了協同程式

在 Android 中,協同程式可協助管理長時間執行的工作。這些工作可能會封鎖主執行緒,導致應用程式沒有回應。有超過 50% 使用協同程式的專業開發人員表示,工作效率有所提升。本主題說明如何使用 Kotlin 協同程式來解決這些問題,藉此編寫更簡潔明瞭的應用程式程式碼。

功能

協同程式是在 Android 上進行非同步程式設計的推薦解決方案。值得注意的功能包括:

  • 輕量:由於支援暫停機制,您可以在單一執行緒中執行許多協同程式,這樣不會封鎖協同程式執行時所處的執行緒。這種暫停機制並不會封鎖執行緒,而是會減少記憶體用量,因此能同時支援多項並行作業。
  • 減少記憶體流失:使用結構化並行來執行範圍內的作業。
  • 內建取消支援:系統會透過執行中的協同程式階層自動傳播取消作業。
  • Jetpack 整合:許多 Jetpack 程式庫包含提供完整協同程式支援的擴充功能。有些程式庫也提供的協同程式範圍,可用於結構化並行。

範例總覽

依據「應用程式架構指南」,這個主題的範例會發出網路要求,並將結果傳回主執行緒,以便應用程式向使用者顯示結果。

具體來說,ViewModel 架構元件會呼叫主執行緒上的存放區層,以觸發網路要求。有許多解決方案都是利用協同程式來確保主執行緒不受封鎖,本指南會逐一進行說明。

ViewModel 包含一組可直接使用協同程式的 KTX 擴充功能。這些擴充功能是 lifecycle-viewmodel-ktx 程式庫,已在本指南中使用。

依附元件資訊

如要在 Android 專案中使用協同程式,請在應用程式的 build.gradle 檔案中新增以下依附元件:

Groovy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

在背景執行緒中執行

如果對主執行緒發出網路要求,會讓主執行緒等待或「封鎖」,直到收到回應為止。由於執行緒遭到封鎖,因此 OS 無法呼叫 onDraw(),導致應用程式凍結列,並可能顯示應用程式無回應 (ANR) 對話方塊。為改善使用者體驗,請在背景執行緒執行這項作業。

首先,我們探討一下 Repository 類別,瞭解它如何發出網路要求:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest 具有同步性質,且會封鎖呼叫執行緒。要建立網路要求的回應模型,我們要有自己的 Result 類別。

ViewModel 會在使用者點擊 (例如按鈕) 時觸發網路要求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

透過上一個程式碼,LoginViewModel 會在發出網路要求時封鎖 UI 執行緒。如要將執行作業移出主執行緒,最簡單的方法是建立新的協同程式,並在 I/O 執行緒上執行網路要求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

我們來研究一下 login 函式中的協同程式程式碼:

  • viewModelScopeViewModel KTX 擴充功能內含的預先定義 CoroutineScope。請注意,所有相協同程式都必須在範圍內執行。CoroutineScope 管理一個或多個相關協同程式。
  • launch 是一種函式,用於建立協同程式,並將函式主體的執行作業調派至相應的調派程式。
  • Dispatchers.IO 表示應在為 I/O 作業保留的執行緒上執行此協同程式。

login 函式的執行方式如下:

  • 應用程式會從主執行緒的 View 層呼叫 login 函式。
  • launch 會建立新的協同程式,而系統會在為 I/O 作業保留的執行緒上單獨發出網路要求。
  • 在協同程式執行期間,login 函式會繼續執行並傳回,這些作業可能在完成網路要求前進行。請注意,為了方便起見,現會忽略網路回應。

由於這個協同程式以 viewModelScope 開頭,所以會在 ViewModel 範圍內執行。如果 ViewModel 因使用者離開畫面而遭刪除,系統會自動取消 viewModelScope,並且一併取消所有執行中的協同程式。

上一個範例的問題是,呼叫 makeLoginRequest 的任何項目都需要注意明確將執行作業從主執行緒中移出。一起來看看如何修改 Repository 來解決這個問題。

使用協同程式,確保主執行緒安全

如果函式沒有封鎖主執行緒上的使用者介面更新,我們會將該函式視為「對主執行緒無威脅」makeLoginRequest 函式並非對主執行緒無威脅,因為從主執行緒呼叫 makeLoginRequest 會封鎖使用者介面。使用協同程式程式庫中的 withContext() 函式,將協同程式的執行作業移至另一個執行緒:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) 會將協同程式的執行作業移至 I/O 執行緒,使呼叫函式對主執行緒無威脅,並讓使用者介面視需要更新。

makeLoginRequest 也標有「suspend」關鍵字。這個關鍵字可讓 Kotlin 強制執行要從協同程式內呼叫的函式。

在以下範例中,協同程式在 LoginViewModel 內建立。當 makeLoginRequest 將執行作業移出主執行緒後,即可在主執行緒中執行 login 函式內的協同程式:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

請注意,由於 makeLoginRequestsuspend 函式,所以此處仍需要該協同程式,且必須在協同程式內執行所有 suspend 函式。

這個程式碼與上一個 login 範例在以下幾方面都不同:

  • launch 不會接收 Dispatchers.IO 參數。當您沒有將 Dispatcher 傳遞至 launch 時,從 viewModelScope 啟動的任何協同程式都會在主執行緒中執行。
  • 系統現在可處理網路要求的結果,以在使用者介面顯示成功或失敗資訊。

login 函式現在的執行方式如下:

  • 應用程式會從主執行緒的 View 層呼叫 login() 函式。
  • launch 會在主執行緒上建立新的協同程式,協同程式也會開始執行。
  • 在協同程式內,對 loginRepository.makeLoginRequest() 的呼叫現在會導致「暫停」進一步執行協同程式,直到 makeLoginRequest()withContext 區塊執行完畢為止。
  • withContext 區塊結束執行後,login() 中的協同程式會繼續在「主執行緒」中執行,並傳回網路要求的結果。

處理例外狀況

如要處理 Repository 層可能擲回的例外狀況,請使用 Kotlin 的內建例外狀況支援。在以下範例中,我們使用 try-catch 區塊:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

在這個範例中,makeLoginRequest() 呼叫擲回的任何非預期例外狀況都會在使用者介面中按錯誤來處理。

其他協同程式資源

如要進一步瞭解 Android 上的協同程式,請參閱「使用 Kotlin 協同程式提升應用程式效能」。

如需取得更多協同程式資源,請參閱下列連結: