07_Android協(xié)程

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 !

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容