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!

发布了32 篇原创文章 · 获赞 36 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/alcoholdi/article/details/103584926