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 {

    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)

    fun <T> json2List(json: String): List<T> {
        return gson.fromJson(json, object : TypeToken<LinkedList<T>>() {}.type)
    }

    fun <T> list2Json(list: List<T>): String {
        return object2Json(list)
    }
}

服务器返回的数据结构

data class Response<T> (
    val code: Int,
    val message: String,
    val result: T
)

创建GsonResponseBodyConverter,用作Response数据转换器

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)
        }
    }
}

创建GsonRequestBodyConverter,用作Request的数据转换

class GsonRequestBodyConverter<T>(
    type: Type
) : Converter<T, RequestBody> {

    private val gson: Gson = JsonUtil.gson
    private val adapter: TypeAdapter<T> = gson.getAdapter(TypeToken.get(type)) as TypeAdapter<T>

    override fun convert(value: T): RequestBody? {
        val buffer = Buffer()
        val writer: Writer =
            OutputStreamWriter(buffer.outputStream(), Charset.forName("UTF-8"))
        val jsonWriter = gson.newJsonWriter(writer)
        adapter.write(jsonWriter, value)
        jsonWriter.close()
        return RequestBody.create(
            MediaType.get("application/json; charset=UTF-8"),
            buffer.readByteString()
        )
    }
}

创建自定义数据转换工厂GsonResponseConverterFactory,这里使用了刚才创建的转换器

object GsonResponseConverterFactory : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type,
        annotations: Array<Annotation?>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *> {
        return GsonResponseBodyConverter<Type>(type)
    }

    override fun requestBodyConverter(
        type: Type,
        parameterAnnotations: Array<out Annotation>,
        methodAnnotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<*, RequestBody> {
        return GsonRequestBodyConverter<Type>(type)
    }
}

配置Retrofit和OkHttp

创建RetrofitClient进行Retrofit和OkHttp配置,并提供API调用

object RetrofitClient {

    private const val Authorization = "Authorization"

    private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(60, 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.Main).launch(CoroutineHandler) {
    val test = RetrofitClient.apiService.test()
    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 {
            http.execute(RetrofitClient.apiService)
        } catch (e: Exception) {
            throw e
        } finally {
            showRound?.let { 
                if (it.isShowing) {
                    it.cancel()
                }
            }
        }
    }

    fun executeAsync(http: HttpPredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
        http.execute(RetrofitClient.apiService)
    }
}

现在可以把之前的请求接口代码用函数式接口这种方式来写,省略了协程和请求动画的代码

HttpRequest.execute(object : HttpPredicate {
    override suspend fun execute(api: ApiService) {
          val test = api.test()
          LogUtil.d("[当前线程为:${Thread.currentThread().name}], 获得数据:${test}")
    }
})

对于函数式接口,我们可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性,最终简化后的请求接口代码如下:

HttpRequest.execute {
    val test = it.test()
    LogUtil.d("[当前线程为:${Thread.currentThread().name}], 获得数据:${test}")
}

不需要请求动画使用executeAsync,不需要更新UI则去掉代码里的线程切换即可。另外为了非请求接口时使用协程的方便也可以把协程的调用单独创建一个工具类,替换HttpRequest类中的协程调用,这样协程的异常全部由CoroutineHandler类来处理了,无需再单独处理。

object CoroutineUtil {

    fun interface CoroutinePredicate {
        suspend fun execute()
    }

    fun execMain(code: CoroutinePredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
        code.execute()
    }

    fun execIO(code: CoroutinePredicate) = CoroutineScope(Dispatchers.IO).launch(CoroutineHandler) {
        code.execute()
    }
}

函数式接口还可以免去创建用Lambda表达式来省略,这里就不描述了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容