Android協(xié)程
????本文以網(wǎng)絡請求為例,由淺入深,來說明協(xié)程在Android中的使用方式。后半部分介紹一些協(xié)程概念。
(1)添加依賴項
????如下:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
(2)網(wǎng)絡請求函數(shù)
????這是一個同步的阻塞函數(shù),調(diào)用它的線程會阻塞。如下:
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"))
}
}
(3)觸發(fā)網(wǎng)絡請求
????用戶點擊時,觸發(fā)網(wǎng)絡請求,如下:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
????此時,會阻塞主線程。通過使用協(xié)程將它移出主線程,如下:
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)
}
}
}
????先說明一下viewModelScope屬性。它是ViewModel的擴展屬性,在androidx.lifecycle.ViewModelKt中定義的。viewModelScope可以對協(xié)程進行管理。Dispatchers.IO說明該協(xié)程是運行在IO線程中的。
????現(xiàn)在基本滿足了要求。但是,對于(2)中的makeLoginRequest()方法來講,如果調(diào)用方忘記了把它從主線程中移出,那么就會出問題。雖然可以通過注釋等方式提醒調(diào)用者,但總有忘記的可能。下面就是杜絕這種可能的方式。
(4)主線程安全
????如果函數(shù)不會阻塞主線程的UI刷新,那么該函數(shù)是主線程安全的。(2)中的makeLoginRequest()不是主線程安全的,使用它時必須移出主線程。下面的方式將它改為主線程安全的:
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
}
}
}
????調(diào)用方:
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()現(xiàn)在是suspend函數(shù),所以必須在協(xié)程中調(diào)用。上面的示例通過viewModelScope.launch 啟動一個協(xié)程來調(diào)用它。注意,該協(xié)程是運行在主線程中的,但不會阻塞主線程。根據(jù)狀態(tài)機的改變,在適當時候再在主線程中執(zhí)行when部分。
(5)異常處理
????通過try-catch來處理異常部分,如下:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun makeLoginRequest(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
}
}
}
}
(6)請求響應處理
????上面的內(nèi)容并沒有涉及請求響應的處理。但一個正常的請求,處理請求響應是必須的。下面就來展示這一點。
when (result) {
is Result.Success<LoginResponse> -> showData(result.data)
else -> showError()
}
suspend fun showData(data : LoginResponse){
withContext(Dispatchers.Main){
val name = data.name
nameText.setText(name)
}
}
suspend fun showError(){
withContext(Dispatchers.Main){
errorView.show()
}
}
????showData()和showError()中都使用了withContext(Dispatchers.Main)來進行線程切換。這是基于調(diào)用它的協(xié)程運行在未知線程上考慮的。如果可以確定運行在主線程,那么不需要進行切換,suspend修飾符也不再需要。如下:
fun showData(data : LoginResponse){
val name = data.name
nameText.setText(name)
}
fun showError(){
errorView.show()
}
????這里并沒有使用JetPack Compose UI的更新方式,而是使用了原View體系。當然,使用Compose方式也是可以的,這里只是為了方便。
(7)launch和async
????協(xié)程的啟動方式有兩種:launch和async。launch啟動新協(xié)程,但不會把結(jié)果返回調(diào)用方。async啟動的協(xié)程允許使用await()函數(shù)返回結(jié)果。通常,launch用于從常規(guī)函數(shù)啟動新協(xié)程,而async是在suspend函數(shù)或者其他協(xié)程內(nèi)使用。
????一個示例如下:
suspend fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
(8)協(xié)程范圍CoroutineScope
???? CoroutineScope用于跟蹤它使用launch和async創(chuàng)建的協(xié)程??梢允褂胹cope.cancel()來取消正在運行的協(xié)程。有一些類有自己的Scope,如ViewModel有viewModelScope,Lifecycle有l(wèi)ifecycleScope。自定義一個CoroutineScope也是可以的,示例如下:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
(9)作業(yè)Job
???? Job是協(xié)程的句柄,可以對協(xié)程進行管理。示例:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
(10)協(xié)程上下文CoroutineContext
???? CoroutineContext使用下面幾個類來定義協(xié)程的行為:
- Job :控制協(xié)程的生命周期;
- CoroutineDispatcher:將工作分配到適當?shù)木€程;
- CoroutineName:協(xié)程名稱;
- CoroutineExceptionHandler:異常處理。
???? 當在一個Scope內(nèi)創(chuàng)建一個協(xié)程時,一個Job instance隨之分配。其他的CoroutineContext相關(guān)元素則從該Scope繼承。覆寫繼承的元素也是可以的,只需要傳遞一個新的CoroutineContext。如下:
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
????Over !