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!