「協同程式」是可在 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
函式中的協同程式程式碼:
viewModelScope
是ViewModel
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
}
}
}
}
請注意,由於 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 協同程式提升應用程式效能」。
如需取得更多協同程式資源,請參閱下列連結: