「協同程式」是可在 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>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // 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 函式中的協同程式程式碼:
viewModelScope是ViewModelKTX 擴充功能內含的預先定義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 */ } } } } }
請注意,由於 makeLoginRequest 是 suspend 函式,所以此處仍需要該協同程式,且必須在協同程式內執行所有 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 協同程式提升應用程式效能」。
如需取得更多協同程式資源,請參閱下列連結: