Retrofit + kotlin coroutines 使用
如何使用 Retrofit
和 coroutine
实现网络请求呢? 下面内容会做一个简单的介绍。
我们使用 ViewModel
处理网络请求,使用 LiveData
监听数据变化。
以下内容分为以下几部分:
- 依赖库版本版本要求
- 利用
suspend
关键字定义API
接口 - 在
ViewModel
中使用coroutines
发起网络请求 - 在
Activity
和Fragment
中通过observer
观察liveData
- 总结
1. 依赖库版本版本要求:
-
retrofit 2.6.0
以上
changeLog
从changeLog
上可以看到,从2.6.0
以上,retrofit
开始支持suspend
关键字,这也是协程的关键。// 现在可以这么声明一个网络接口 @GET("users/{id}") suspend fun user(@Path("id") id: Long): User
-
kotlin-coroutines-android
除了依赖kotlin
外,需要引入协程相关库api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2' api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
2. 定义 API
接口
如上面所示,API
接口的样子为:
interface NewService {
/**
* 首页的 banner 的请求
*/
@GET("/banner/json")
suspend fun getBanner(): Response<HomeBanner>
@GET("/article/list/{page}/json")
suspend fun getArticleList(@Path("page") page: Int): Response<HomeListResponse>
}
上述接口为
https://www.wanandroid.com/
的开放api
实用 suspend
关键字标注方法,并且返回类型为 Response
包裹的 所需要的数据类型
.
和正常使用的 retrofit
接口唯一不同的地方是使用了 suspend
关键字。
suspend
是一个标志,告诉该函数的调用者,「这个函数」是一个耗时的操作,必须放入在协程中调用。
有关协程的仔细解析,我会放在另外一篇文章去写。
3. 在 ViewModel
中发起网络请求
首先在 Activity
或者 Fragment
中如何获取到 ViewModel
val firstHomeVM = ViewModelProviders.of(this).get(FirstHomeViewModel::class.java)
// 当需要获取数据时,如下
firstHomeVM.getBannerData()
在 FirstHomeViewModel
中 请求的网络访问:
以 getBannerData()
为例:
/**
* 获取首页 banner 信息
*/
fun getBannerData() {
// 第一处
viewModelScope.launch(IO) {
// 第二处
val result = getBannerUseCase.getWanAndroidBanner()
if (result is Result.Success) {
// 第三处
withContext(Main) {
emitUIBanner(result.data)
}
} else if (result is Result.Error) {
withContext(Main) {
emitUIEmptyBanner()
}
}
}
}
如代码所示, 我们标注了三处地方:「第一处」,「第二处」,「第三处」, 下面分析一下这几处的代码实现。
3.1 第一处:viewModelScope
首先,代码中 viewModelScope
来自 liftcycle-viewmodel-ktx-2.2.0
,是 ViewModel
的一个扩展属性,当 ViewModel
被 cleared
时,viewModelScope
会被 cancel
掉。
在 kotlin
中,协程总是运行在以 CoroutineContext
类型为代表的上下文中。
在这个里面, launch()
方法有三个参数:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
前两个参数都有对应的默认值,这里我们传入的是一个闭包 block
,在闭包中,是我们真正需要执行的逻辑。
-
CoroutineContext
: 如果我们不传递,默认会运行在viewModelScope
所在的线程 ->main
线程, 在这里我们指定为IO
线程。
3.2 第二处:getBannerUseCase.getWanAndroidBanner()
这里是获取网络数据的地方。
因为我们标明了 IO
, 那么协成会帮我们切到切到子线程中执行 getWanAndroidBanner()
,
并且由于是挂起函数 suspend
, 会在这个位置挂起当前线程,切到其他线程「主线程」做其他的事。
当结果 result
回来时,会继续执行下面的 if (result ...)
代码。
getWanAndroidBanner()
的本质上是进行了网络访问, 代码可简单写为
/**
* 获取 banner 信息
*/
suspend fun getWanAndroidBanner(): Result<List<HomeBanner.BannerItemData>> {
var result
val bannerResult = wanService.getBanner()
// 成功时
if (bannerResult.isSuccessful && bannerResult.body() != null) {
val body = bannerResult.body()
result = Result.Success(body)
} else {
result = Result.Error(Exception("获取 banner 失败 error code ${bannerResult.code()} error body is ${bannerResult.errorBody()} "))
}
// 再次处理 result
...
Log.i("zc_test", "hahahha current thread is ${Thread.currentThread()}")
return result
}
上述代码中的 Result
是我本地写的一个,统一对返回的结果进行了包装。
3.3 第三处:切换现场到主线程
withContext()
是 kotlinx-coroutines-core
中提供的一个 suspend
挂起函数,便于我们切换线程。
Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
)
在这里会挂起,并且执行特定的闭包「我们传入的 block
」, 直到 block
运行完。
在我们的代码中:
withContext(Main) {
...
emitUIBanner(result.data)
}
// emitUIBanner(result.data) 的代码实现
/**
* 在 ui 现场中调用,刷新 banner
*/
UiThread
private fun emitUIBanner(banners: List<HomeBanner.BannerItemData>) {
bannerUILD.value = banners
}
我们切换该闭包在 Main
主线程中执行,里面的代码,本质上是刷新 UI
的逻辑。
利用 LiveData
的 setValue()
方法,通知外部可刷新 UI
了。
上面我们看到了 Main
和 IO
, 是我们分别指定的协程调用器,协程会在指定的调度器上运行,并且会自动切换线程。
4. 在 Activity
和 Fragment
中通过 observer
观察 liveData
上面我们已经获取到了数据,并且通过 LiveData.setvalue()
设置了数据,那么我们得需要接收方拿到该数据后才会触动刷新操作。
在 Activity
和 Fragment
中通过 observer
观察 liveData
.
代码如下:
// banner 成功的 监听
val bannerLDObserver = Observer<List<HomeBanner.BannerItemData>> {
// 刷新 UI
bannerList.addAll(it)
bannerDataList.addAll(it)
startSwitchJob()
bannerAdapter.notifyDataSetChanged()
// 刷新 UI
home_swipe_refresh.isRefreshing = false
}
firstHomeVM.bannerUILD.observe(this, bannerLDObserver)
通过上面四个步骤,我们实际上完成了「获取数据」和「更新 UI
」的操作。
也是我们本次想要分享的内容,如果利用 kotlin coroutines
和 retrofit
实现网络请求。
5. 总结
上述简单的说明了,利用 kotlin coroutines
如何实现一些网络请求,对于耗时的一些操作我们都可以使用 kotlin coroutines
去实现。
至于为什么使用 kotlin coroutines
这里就不再讨论。
2019.12.5 by chendroid
水一下~
希望尽快详细写一篇有关协程的文章。
balabala