Android kotlin 使用協程配合Retrofit進行網絡請求

Project build.gradle

apply from: "config.gradle"

buildscript {
   ext.kotlin_version = '1.3.61'
   repositories {
       jcenter()
       google()
   }
   dependencies {
       classpath 'com.android.tools.build:gradle:3.5.3'
       classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
   }
}

allprojects {
   repositories {
       jcenter()
       google()
   }
}



Module build.gradle

apply plugin: 'kotlin-android'

android {

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2'
}



Retrofit Service 的聲明如下

interface ZhihuApiService {

    companion object Factory {

        val ZHIHU_BASE_URL = "http://news-at.zhihu.com/api/"

        val mZhihuApiService = create()

        fun create(): ZhihuApiService {

            val okHttpClient = OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).build()

            val retrofit = Retrofit.Builder()
                    .client(okHttpClient)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .baseUrl(ZHIHU_BASE_URL)
                    .build()

            return retrofit.create(ZhihuApiService::class.java)
        }
    }

    @GET("3/news/hot")
    suspend fun getHotCoroutine(): HotJson

}



協程訪問網絡請求的基礎寫法

private fun getHot() {
    GlobalScope.launch(Dispatchers.Main) {

        val hotJson = withContext(Dispatchers.IO){
            val dailiesJson = ZhihuApiService.mZhihuApiService.getHotCoroutine()
            dailiesJson
        }

        if (swipe_refresh_layout.isRefreshing) {
            swipe_refresh_layout.isRefreshing = false
        }

        val hots = hotJson.hots
        if (hots != null) {
            hotAdapter!!.addList(hots)
            hotAdapter!!.notifyDataSetChanged()
        }
    }
}




這個很基礎的寫法有個很基礎的問題,如果網絡報錯,比如最簡單的 Timeout 超時錯誤,會導致代碼報錯,應用崩潰。
所以需要用 try catch 包裹一層。

協程訪問網絡請求的完整的寫法

private fun getHot() {
    GlobalScope.launch(Dispatchers.Main) {

        try {
            val hotJson = withContext(Dispatchers.IO){
                val dailiesJson = ZhihuApiService.mZhihuApiService.getHotCoroutine()
                dailiesJson
            }

            val hots = hotJson.hots
            if (hots != null) {
                hotAdapter!!.addList(hots)
                hotAdapter!!.notifyDataSetChanged()
            }
        } catch (e: Exception) {
            Log.e("YAO", e.toString())
        } finally {
            if (swipe_refresh_layout.isRefreshing) {
                swipe_refresh_layout.isRefreshing = false
            }
        }
    }
}

完整代碼在這裏




另一個開發者的版本

網上作者 秉心說 ,封裝了一層,按照了這種流程進行了封裝。代碼可讀性提高了一些。
WanService.kt

interface WanService {

    @FormUrlEncoded
    @POST("/user/login")
    suspend fun login(@Field("username") userName: String, @Field("password") passWord: String): WanResponse<User>

}

BaseRepository.kt

open class BaseRepository {

    suspend fun <T : Any> apiCall(call: suspend () -> WanResponse<T>): WanResponse<T> {
        return call.invoke()
    }

    suspend fun <T : Any> safeApiCall(call: suspend () -> Result<T>, errorMessage: String): Result<T> {
        return try {
            call()
        } catch (e: Exception) {
            // An exception was thrown when calling the API so we're converting this to an IOException
            Result.Error(IOException(errorMessage, e))
        }
    }
}

LoginRepository.kt

class LoginRepository : BaseRepository() {

    suspend fun login(userName: String, passWord: String): Result<User> {
        return safeApiCall(call = { requestLogin(userName, passWord) },
                errorMessage = "登錄失敗!")
    }

}

Result.kt

sealed class Result<out T : Any> {

    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()

    override fun toString(): String {
        return when (this) {
            is Success<*> -> "Success[data=$data]"
            is Error -> "Error[exception=$exception]"
        }
    }
}

LoginViewModel.kt

fun login() {
    viewModelScope.launch(Dispatchers.Default) {
        if (userName.get().isNullOrBlank() || passWord.get().isNullOrBlank()) return@launch

        withContext(Dispatchers.Main) { showLoading() }

        val result = repository.login(userName.get() ?: "", passWord.get() ?: "")

        withContext(Dispatchers.Main) {
            if (result is Result.Success) {
                emitUiState(showSuccess = result.data, enableLoginButton = true)
            } else if (result is Result.Error) {
                emitUiState(showError = result.exception.message, enableLoginButton = true)
            }
        }
    }
}

我大概理解一下:
1、用關鍵字 sealed class 聲明一個「密封類 Result 」把 「請求正常返回的結果(泛型)」 和 「異常」綁在了一起。這樣纔可以實現調用 login 這樣的業務請求方法統一返回一個結果。
2、網絡請求通過 safeApiCall 進行了封裝一層厚調用,safeApiCall 就是 try catch 的封裝。
3、login 方法體,先運行在了一個 Dispatchers.Default 裏,這是子線程。(我感覺運行在 Dispatchers.IO 裏更合適)。網絡請求前,先在主線程裏顯示加載畫面,然後執行網絡請求 repository.login。拿到結果後,又在主線程裏,if&else 判斷 Result 是密封類裏的哪種類型,執行對應的UI更新。




我的感想,關於 協程 vs RxJava,到底誰更方便

可以看到用協程請求網絡,縮進的情況也不少。並不能達到多少「減少回調從而減少嵌套&縮進,增加代碼可讀性」的效果。
所以比較一下下面 協程 和 RxJava 兩種寫法,對於 RxJava 使用熟練的開發者,反而覺得 RxJava 更好理解,更有規範,代碼封裝得更好。
而且 RxJava裏面的操作符 比 Kotlin語言自身帶有的操作符,更多更完善,能方便我們更好的寫業務代碼。

//協程版本
private fun getHot() {
    GlobalScope.launch(Dispatchers.Main) {

        try {
            val hotJson = withContext(Dispatchers.IO){
                val dailiesJson = ZhihuApiService.mZhihuApiService.getHotCoroutine()
                dailiesJson
            }

            val hots = hotJson.hots
            if (hots != null) {
                hotAdapter!!.addList(hots)
                hotAdapter!!.notifyDataSetChanged()
            }
        } catch (e: Exception) {
            Log.e("YAO", e.toString())
        } finally {
            if (swipe_refresh_layout.isRefreshing) {
                swipe_refresh_layout.isRefreshing = false
            }
        }
    }
}

//Rxjava版本
private fun getHotBak() {
    ZhihuHttp.mZhihuHttp.getHot().subscribe(object : Observer<HotJson> {

        override fun onSubscribe(@NonNull d: Disposable) {
            mDisposable = d
        }

        override fun onNext(hotJson: HotJson) {
            val hots = hotJson.hots
            if (hots != null) {
                hotAdapter!!.addList(hots)
                hotAdapter!!.notifyDataSetChanged()
            }
        }

        override fun onComplete() {

        }

        override fun onError(e: Throwable) {
            Logger.e(e, "Subscriber onError()")
        }
    })
}



關於協程的理解,一句話總結

kotlin 協程 是一個線程框架,協程就是切線程

  • 可以理解爲協程是 jvm線程Api 的一個很好的封裝,協程不能獨立於線程外,它也是需要跑在線程中的。
  • Thread 是最底層的組件,Executor 和 Coroutine 都是基於它所創造出來的工具包。



關於 Dispatchers,有以下幾種常用的
  • Dispatchers.Main:Android 中的主線程
  • Dispatchers.IO:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求
  • Dispatchers.Default:適合 CPU 密集型的任務,比如計算




參考
扔物線 Kotlin 的協程用力瞥一眼
Kotlin 協程的掛起好神奇好難懂?今天我把它的皮給扒了
到底什麼是「非阻塞式」掛起?協程真的更輕量級嗎?
真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章