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 協同程式提升應用程式效能」。

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