现在Android端网络访问基本使用的都是okhttp或retrofit第三方库,retrofit库对okhttp库进行了再次封装与改进,并在2.6.0版本开始,retrofit增强了对kotlin语言的兼容性,并内置了对kotlin协程的支持,使得retrofit+kotlin组合使用更加轻便简洁,这篇文章将介绍retrofit 2.6.0之前和之后使用kotin协程异步请求网络的步骤,通过对比你会体现到升级后的极简体验。
这里借《第一行代码》的天气预报项目来讲述。
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
这里引入了retrofit 2.6.0版本,converter-gson转换器使用gson库集合retrofit将网络访问数据直接转换为对应的实例实体对象,kotlinx-coroutines-core 为 Coroutine 的核心 API, 而kotlinx-coroutines-android 为安卓平台的一些提供了一些支持,特别是提供了 Dispatchers.Main 这个 UI Dispatcher。
class DailyResponse(val status: String, val result: Result) {
class Result(val daily: Daily)
class Daily(val temperature: List<Temperature>, val skycon: List<Skycon>, @SerializedName("life_index") val lifeIndex: LifeIndex)
class Temperature(val max: Float, val min: Float)
class Skycon(val value: String, val date: Date)
class LifeIndex(val coldRisk: List<LifeDescription>, val carWashing: List<LifeDescription>, val ultraviolet: List<LifeDescription>, val dressing: List<LifeDescription>)
class LifeDescription(val desc: String)
}
天气相关的接口(根据经纬度):
interface WeatherService {
@GET("v2.5/${SunnyWeatherApplication.TOKEN}/{lng},{lat}/realtime.json")
fun getRealtimeWeather(@Path("lng") lng: String, @Path("lat") lat: String): Call<RealtimeResponse>
@GET("v2.5/${SunnyWeatherApplication.TOKEN}/{lng},{lat}/daily.json")
fun getDailyWeather(@Path("lng") lng: String, @Path("lat") lat: String): Call<DailyResponse>
}
${}表示取变量值,{lng}表示标记了lng变量用于传入具体数值,在对应函数接收参数中使用@Path("")注解将传入变量替换{lng}的位置。函数返回类型为指定泛型的Call接口。
interface PlaceService {
@GET("v2/place?token=${SunnyWeatherApplication.TOKEN}&lang=en_US")
fun searchPlaces(@Query("query") query: String): Call<PlaceResponse>
}
@QUery("")用于条件字段参数,作用于方法参数(主要在GET中使用),这里替换后实际请求的网址为:
若query多个参数,如这个网址:
http://102.10.10.132/api/News?newsId=1&type=类型1
则接口这样描述:
@GET("News")
Call<NewsBean> getItem(@Query("newsId") String newsId, @Query("type") String type);
更多retrofit注解参考:Retrofit网络请求参数注解
object ServiceCreator {
private const val BASE_URL = "https://api.caiyunapp.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
//inline内联 物化reified 使得泛型能获取自身的Class类型
inline fun <reified T> create(): T = create(T::class.java)
}
这里使用object单例类,使得retrofit只有一份实例,同时这里使用了inline来修饰方法(inline编译时就会将内联函数的代码替换到实际调用的地方,则不存在泛型擦除),reified来修饰泛型从而实现了泛型实化(获取泛型的具体class类型,这在Java中是不允许的,具体参考:kotlin之泛型的实化、协变、逆变 )
//搜索地点
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
private suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
这里对Call接口扩展了一个await属性方法用于实现在协程中的网络访问,这里使用suspend标记为挂起函数,使用suspendCoroutine(必须在协程作用域或挂起函数中调用,接收一个lambda表达式,开启子线程执行代码同时阻塞当前协程,表达式参数有一个Continuation对象,调用resume()或resumeWithException()恢复协程执行),由于这里调用时传入的Call指定了T的具体类型,所以调用serachPlaces方法可直接得到对应的数据实体对象。
fun searchPlaces(query: String) = fire(Dispatchers.IO) {
val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
if (placeResponse.status == "ok") {
val places = placeResponse.places
Result.success(places)
} else {
Result.failure(RuntimeException("response status is ${placeResponse.status}"))
}
}
private fun <T> fire(context: CoroutineContext, block: suspend () -> Result<T>) =
liveData<Result<T>>(context) {
val result = try {
block()
} catch (e: Exception) {
Result.failure<T>(e)
}
emit(result)
}
其中封装的fire函数第一个参数context指定协程作用域所在线程,第二个参数block是一个函数参数,然后类型为Result,在livedata(){}代码块中拥有了协程作用域,result变量接收block执行结果得到Result对象,调用emit(result)封装LiveData<>数据,则fire函数将LiveData
这里使用一个简单get接口返回来描述。
postman请求如下:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
这里使用2.9.0版本的retrofit。
data class urlData(val address:String,val time:String)
interface urlInterface {
@GET("lit/geturl")
suspend fun getUrl(): UrlData
}
注意这里将接口方法声明为suspend,同时返回类型直接设置为数据类,retrofit内部直接封装了协程请求网络,数据解析,线程切换,极大的精简了业务代码。
var retrofit = Retrofit.Builder()
.baseUrl("http://45.32.72.37:8081/") //设置BaseURL需以'/'结尾
.addConverterFactory(GsonConverterFactory.create()) //json解析器Gson
.callbackExecutor(Executors.newSingleThreadExecutor()) //使用单独的线程
.build() //创建Retrofit对象
MainScope().launch {
val api = MyApplication.retrofit.create(urlInterface::class.java) //创建Api代理对象
var data = api.getUrl()
Log.w("Retrofit_test","网址${data.address}时间${data.time}")
webview.loadUrl("${data.address}")
}
由于接口的geturl声明为suspend函数,这里需要开启一个协程作用域来调用,MainScope用于在主线程开启作用域,由于retrofit底层的处理,此段代码和前面的一样实现了异步请求同步化,在该作用域中,执行getUrl()函数期间协程处于挂起状态,等待结果返回后恢复执行,同时由于mainScope作用域下处于主线程,可以直接更新UI,至于这里为什么使用mainScope构建作用域,下面会进行说明。
通常使用GlobalScope.launch() 启动一个协程,Globalscope 通常启动顶级协程,这些协程在整个应用程序生命周期内运行,不会被过早地被取消。程序代码通常应该使用自定义的协程作用域。直接使用 GlobalScope 的 async 或者 launch 方法是强烈不建议的。
查看MainScope()创建源码:
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
我们发现MainScope默认的调度器是主线程,所以就可以直接在其作用域更新UI了。
s如果项目采用的是MVVM架构的话,上述例子就不适用了,这里介绍下MVVM在Android中的模式:
所以在ViewModel中使用viewModelScope 来进行协程相关操作,同时当 ViewModel.onCleared() 被调用的时候,viewModelScope 会自动取消作用域内的所有协程。
Kotlin 同样为 LiveData 赋予了直接使用协程的能力。在第一个网络访问举例中就使用了Livedata的协程作用域,直接在 liveData {} 代码块中调用需要异步执行的挂起函数,并调用 emit() 函数发送处理结果。当 LiveData 进入 active 状态时,liveData{ } 会自动执行。
当 LifeCycle 回调 onDestroy() 时,协程作用域 lifecycleScope 会自动取消。在 Activity/Fragment 等生命周期组件中我们可以很方便的使用。
suspend fun <T> Lifecycle.whenCreated()
suspend fun <T> Lifecycle.whenStarted()
suspend fun <T> Lifecycle.whenResumed()
suspend fun <T> LifecycleOwner.whenCreated()
suspend fun <T> LifecycleOwner.whenStarted()
suspend fun <T> LifecycleOwner.whenResumed()
这种特殊用法可以指定至少在特定的生命周期之后再执行挂起函数,可以进一步减轻 View 层的负担。
jetpack组件和retrofit对kotlin语言进行了很多便捷的扩展,使用需要引入相应的ktx扩展库即可感受到极大的编码简洁化。同时在以往采用okhttp+Thread访问网络需要大量的模板代码和异常处理,但采用retrofit+协程来实现的话会很大的便利开发者实现具体业务。