Android 使用Retrofit+协程+函数式接口实现傻瓜式
Retrofit 是Square公司为Android和Java开发的类型安全的Http Client。Retrofit 专注于接口的封装,OkHttp 专注于网络请求的高效,二者分工协作!使用 Retrofit 请求网络,实际上是先使用 Retrofit 接口层封装请求参数、Header、Url 等信息,然后由 OkHttp 完成后续的请求操作,最后在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit 根据用户的需求对结果进行解析
Retrofit 虽然使用起来还算简单,但每个接口都需要写回调函数比较繁琐,就算使用协程的挂起函数简化了写法,但处理请求错误、请求动画、协程的创建与切换等操作还是使得一个简单的请求需要写一大篇额外代码,本篇主要是通过函数式接口简化了这些代码的编写,废话不多说直接上代码
用到的依赖和权限
在AndroidManifest.xml
中添加
<uses-permission android:name="android.permission.INTERNET" />
在build.gradle文件的dependencies中添加
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
自定义协程异常处理
创建自定义异常RequestException
作为请求时服务器内部错误使用
class RequestException constructor(
response: String
) : RuntimeException(response)
创建单例CoroutineHandler
作为协程异常处理类
object CoroutineHandler: AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) {
// 打印
exception.printStackTrace()
// 处理
when(exception.javaClass.name) {
ConnectException::class.java.name -> ToastUtil.show("请求异常,请检查网络")
RequestException::class.java.name -> {
// 处理服务器错误
}
...
}
context.cancel()
}
}
自定义数据转换工厂
简单封装一下Gson
object JsonUtil {
private val gson: Gson = Gson()
fun <T> object2Json(obj: T): String = gson.toJson(obj)
fun <T> json2Object(json: String, obj: Type): T = gson.fromJson(json, obj)
fun <T> json2Object(json: String, obj: Class<T>): T = gson.fromJson(json, obj)
...
}
服务器返回的数据结构
data class Response<T> (
val code: Int,
val message: String,
val result: T
)
创建GsonResponseBodyConverter
,用做数据转换器
class GsonResponseBodyConverter<T>(
private val type: Type
) : Converter<ResponseBody, T> {
override fun convert(value: ResponseBody): T {
val response = value.string()
LogUtil.dj(response)
val httpResult = JsonUtil.json2Object(response, Response::class.java)
// 这里是定义成code 200为正常,不正常则抛出之前定义好的异常,在自定义的协程异常处理类中处理
return if (httpResult.code == 200) {
JsonUtil.json2Object(response, type)
} else {
throw RequestException(response)
}
}
}
创建自定义数据转换工厂GsonResponseConverterFactory
,这里使用了刚才创建的转换器
object GsonResponseConverterFactory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation?>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return GsonResponseBodyConverter<Any>(type)
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<out Annotation>,
methodAnnotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody>? {
return super.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
}
}
配置Retrofit和OkHttp
创建RetrofitClient
进行Retrofit和OkHttp配置,并提供API调用
object RetrofitClient {
private const val Authorization = "Authorization"
private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.callTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain: Interceptor.Chain ->
// 这里配置了统一拦截器用以添加token 如果不需要可以去掉
val request = chain.request().newBuilder().apply {
PreferenceManager.getToken()?.let {
addHeader(Authorization, it)
}
}.build()
LogUtil.d("request: ${request.method()} ${request.url()} ${request.body()}\n" +
"headers: ${request.headers()}")
chain.proceed(request)
}
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://poetry.apiopen.top/")
// 放入之前写好的数据转换工厂
.addConverterFactory(GsonResponseConverterFactory)
.client(okHttpClient)
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
创建ApiService用来编写服务器的接口提供给retrofit.create()
使用,具体语法可以参考Retrofit官网
interface ApiService {
// 直接使用的网上接口,用作测试
@GET("sentences")
suspend fun test(): Response<User>
}
测试用的实体类
data class User(
private var name: String,
private var from: String
)
写到这里,已经通过自定义的数据转换和协程异常处理简化了验证数据的一部分代码了,创建协程包裹住然后通过RetrofitClient.apiService.test()
已经可以直接发起请求了,如下所示:
// 这里使用了CoroutineHandler对异常进行处理
CoroutineScope(Dispatchers.IO).launch(CoroutineHandler) {
val test = RetrofitClient.apiService.test()
withContext(Dispatchers.Main) {
// 更新ui
}
LogUtil.d("[当前线程为:${Thread.currentThread().name}], 获得数据:${test}")
}
打印的数据如下,可以看到获取到的数据已经是可以直接使用的数据,但代码还是不够简洁,每次都需要手动创建一个协程,以及写上RetrofitClient.apiService
,如果想让用户体验更好还要再加上请求动画的代码。总体来说还是有一些繁琐,是否能把这些也省掉呢?我想应该是可以的。
通过函数式接口简化重复代码
函数式接口的作用是什么?简单以下面代码来说,可以在使用HttpPredicate
做参数的方法里传入一段代码,这段代码里可以使用execute
方法的参数,这段代码会传入调用HttpPredicate.execute
的地方,是不是刚好符合要简化的需求?把具体要写的代码提出来,把重复的业务逻辑放进去,就大功告成了
用 fun 修饰符可以在 Kotlin 中声明一个函数式接口,这里创建函数式接口HttpPredicate
,用suspend
声明这是挂起函数
fun interface HttpPredicate {
suspend fun execute(api: ApiService)
}
创建单例类HttpRequest
把重复的逻辑抽取出来,这里创建两个方法,execute
是有请求动画的,executeAsync
是没有的,DialogManager.showRound()
请求动画的具体实现可以查看《Android 用 AlertDialog 实现页面等待提示》
object HttpRequest {
fun execute(http: HttpPredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
val showRound = DialogManager.showRound()
try {
request(http)
} catch (e: Exception) {
throw e
} finally {
if (showRound!!.isShowing) {
showRound.cancel()
}
}
}
fun executeAsync(http: HttpPredicate) = CoroutineScope(Dispatchers.IO).launch(CoroutineHandler) {
request(http)
}
private suspend fun request(http: HttpPredicate) = coroutineScope {
val async = async(Dispatchers.IO) {
// 传入代码块,RetrofitClient.apiService作为参数
http.execute(RetrofitClient.apiService)
}
async.await()
}
}
现在可以把之前的请求接口代码用函数式接口这种方式来写,省略了协程和请求动画的代码
HttpRequest.execute(object : HttpPredicate {
override suspend fun execute(api: ApiService) {
val test = api.test()
withContext(Dispatchers.Main) {
// 更新ui
}
LogUtil.d("[当前线程为:${Thread.currentThread().name}], 获得数据:${test}")
}
})
对于函数式接口,我们可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性,最终简化后的请求接口代码如下:
HttpRequest.execute {
val test = it.test()
withContext(Dispatchers.Main) {
// 更新ui
}
LogUtil.d("[当前线程为:${Thread.currentThread().name}], 获得数据:${test}")
}