最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

Shine——更简单的Android网络请求库封装

热点资讯 loading 295浏览 0评论

Shine——更简单的Android网络请求库封装

写在前面

距离上一篇文章跟我一起开发商业级IM(3)—— 长连接稳定性之连接及重连发布的时间,大概已有一年多,先跟大家说声抱歉。主要是因为工作太忙,业务需求过多,没办法专心写博客。先立个Flag:IM系列文章一定会坚持写完,同时Github项目也会逐步完善,敬请期待。
这次就暂不更新IM系列相关的文章及项目了,先给大家带来一个稍微轻量级同时也比较实用的网络请求封装库:Shine,同时也希望自己借此机会重新拾起写博客和开源项目的激情,废话少说,我们直接开始吧。

Shine是什么?

基于Retrofit二次封装的网络请求库。通过统一封装、高内聚、低耦合、灵活配置、高度扩展等特性使Android网络请求更简单。

  • 版本
    • Java Retrofit+RxJava
    • Kotlin Retrofit+Coroutine

Shine能做什么?

  • 支持的请求
    • GET
    • POST
    • PUT
    • DELETE
  • 支持动态BaseUrl
  • 支持自定义Response Model(不同数据结构)
  • 支持自定义Response Parser(响应数据解析器)
  • 支持自定义Cipher(请求/响应数据加解密)
  • 支持自定义Content Type
  • 支持异步/同步请求
  • 统一的IApiService,新增接口时无需改动IApiService
  • 统一的异常处理,方便在接口请求失败时获取相关错误信息

为什么这样做?

  • 不统一的Response Model

日常开发中,大家应该会经常遇到Response Model不统一的情况,例如服务端A返回的数据格式为:code、msg、data,服务端B返回的数据格式为:errCode、errMsg、result,服务端C返回的数据格式为status、message、data等,甚至即使是同一个服务端提供的接口,也可能存在不同接口返回不同数据格式,客户端兼容起来异常困难。在Shine中,通过自定义Response Model及Response Parser即可轻松解决此问题,同时支持配置全局Response Model及Response Parser,适应大多数单个服务器域名及返回数据格式的场景。

  • 动态BaseUrl

日常开发中,难免需要对接不同的服务器。Shine通过内部封装,使BaseUrl及Retrofit实例一一对应,应用层可配置全局BaseUrl或单个接口动态传递BaseUrl,使用灵活简单。

  • 统一的IApiService

通常情况下,使用Retrofit请求接口的步骤为:

  1. 定义IApiService,声明接口;
  2. 在Model或Repository层调用接口;
  3. 在Presenter或ViewModel层调用Model实现的接口。

在Shine中,抽象为通用的IApiService,通过定义统一的get()/post()/put()/delete()/syncGet()/syncPost()/syncPut()/syncDelete()等接口,实现通用的IApiService,在新增接口或旧接口发生变动时,无需修改IApiService,降低开发成本并提升开发效率。

  • 灵活的请求/响应Cipher(数据加解密器)

可配置全局Cipher或单个接口动态传递Cipher,灵活实现接口请求及响应数据加解密功能。例如接口A数据加密方式为AES,接口B数据加密方式为RSA等。

  • 异步/同步请求支持

提供异步/同步请求方式支持。异步请求接口是我们平时请求的常用方式,但某些情况下,需要同步请求方式以实现某些需求,例如Ali OSS StsToken获取等。

  • 统一的异常处理

通过封装RequestException实现统一异常处理,调用方仅需在自定义Response Model时构造对应的RequestException并传入错误码、错误信息等参数,使用Shine在接口请求失败时,通过RequestException提供的错误信息对业务做异常处理即可。

设计、封装思路及原理

  • 项目结构 com.freddy.shine.kotlin
    • cipher(数据加解密器相关)
    • config(配置相关)
    • exception(异常相关)
    • interf(抽象接口相关)
    • model(Response Model相关)
    • parser(数据解析器相关)
    • retrofit(Retrofit相关)
    • utils(工具类相关)
    • AbstractRequestManager.kt(RequestManager抽象类,自定义RequestManager需继承此类)
    • RequestManagerFactory.kt(RequestManager工厂,提供获取RequestManager方法,应用层直接调用[getRequestManager]即可,无需关心内部实现逻辑)
    • ShineKit.kt(Shine核心类)
  • 设计及封装

Shine内部封装请求逻辑,同时提供以下方案使Shine更易用、更具扩展性:

  • 暴露ICipher接口使调用方灵活自定义相关数据加解密器实现,并可配置全局/单个接口请求使用;
  • 暴露IParser接口使调用方灵活自定义相关数据解析器实现,并可配置全局/单个接口请求使用;
  • 抽象统一的IApiService,支持异步/同步请求,并统一请求方式使Shine支持各项目使用;
  • 内部多Retrofit实例管理使Shine支持动态BaseUrl;
  • 通过构建者模式使Shine请求调用参数传递更灵活等。
  • 原理
  • Retrofit多实例管理:采用Map保存多个Retrofit实例,key: BaseUrl, value: Retrofit Instance。当然有些同学可能觉得多个Retrofit会造成性能浪费、不好管理之类的,这个就见仁见智了。我觉得在一个项目中BaseUrl并不会过多,并且如果是统一的OkHttpClient的话,多个Retrofit实例并不会造成多大的性能浪费,并且多个Retrofit反而会更灵活。当然,后续我会增加移除Retrofit实例的接口,大家如果觉得在某个时刻(大概率不再请求该BaseUrl)可以适当移除该Retrofit实例的话直接移除即可,即使会重新请求,那也就是重新创建一个Retrofit实例而已(详见RetrofitManager.kt)。
  • 动态请求头:通过自定义OkHttp Interceptor获取请求Url实现Request Headers传递(详见OkHttpRequestHeaderInterceptor.kt)。
  • 自定义数据加解密器:通过自定义OkHttp Interceptor同时暴露ICipher接口使调用方灵活自定义请求/响应数据加解密器(详见OkHttpRequestEncryptInterceptor.ktOkHttpResponseDecryptInterceptor.ktDefaultCipher.kt)。
  • 自定义数据解析器:通过反射获取Parser实例,获取到Parser实例后会保存到Map方便下一次获取。同时暴露IParser接口使调用方灵活自定义数据解析器(详见AbstractRequestManager.ktDefaultParser.kt)。
  • Java泛型擦除问题:大家应该有遇到过,在Java中无法传递ArrayList.class。在Kotlin中,可以通过inline及reified关键字获取泛型T class,但在Java中会存在泛型擦除的问题(关于Java泛型擦除大家可自行了解,在此不再展开),为了解决此问题,通过自定义ParameterizedTypeImpl实现ParameterizedType接口即可(详见TypeUtil.java及Demo中BaseRepository.java调用)。

参数及API说明

  • RequestOptions
参数名称 说明 类型 示例 默认值 备注
requestMethod 请求方式 RequestMethod RequestMethod.GET RequestMethod.GET /
baseUrl 服务器域名 String api.oick.cn/ / /
function 接口地址 String article/list/0/json / /
headers 请求头 ArrayMap<String, Any> / / /
params 请求参数 ArrayMap<String, Any> / / /
contentType 内容类型 String application/json; charset=utf-8 application/json; charset=utf-8 /
  • ShineOptions
参数名称 说明 类型 示例 默认值 备注
logEnable Shine日志开关 Boolean true true /
logTag Shine日志TAG String Custom Shine /
baseUrl Shine默认服务器域名 String / / 配置后,当某个接口没有动态设置BaseUrl时,将会用此默认BaseUrl
parserCls Shine默认数据解析器 KClass DefaultParser::class DefaultParser::class 配置后,当某个接口没有动态设置Parser时,将会用此默认Parser
  • IRequest
/**
 * 抽象的接口请求封装,自定义RequestManager实现此接口即可
 *
 * @author: FreddyChen
 * @date  : 2022/01/07 13:47
 * @email : freddychencsc@gmail.com
 */
interface IRequest {

    /**
     * 异步请求
     * @param options   请求参数
     * @param type      数据类型映射
     * @param parserCls 数据解析器
     * @param cipherCls 数据加解密器
    */
    suspend fun <T> request(
        options: RequestOptions,
        type: Type,
        parserCls: KClass<out IParser>,
        cipherCls: KClass<out ICipher>? = null
    ): T

    /**
     * 同步请求
     * @param options   请求参数
     * @param type      数据类型映射
     * @param parserCls 数据解析器
     * @param cipherCls 数据加解密器
    */
    fun <T> syncRequest(
        options: RequestOptions,
        type: Type,
        parserCls: KClass<out IParser>,
        cipherCls: KClass<out ICipher>? = null
    ): T
}
  • ICipher
/**
 * 加解密器抽象接口
 *
 * @see [DefaultCipher]
 * @author: FreddyChen
 * @date  : 2022/01/13 16:07
 * @email : freddychencsc@gmail.com
 */
interface ICipher {

    /**
     * 加密数据
     */
    fun encrypt(original: String?): String?

    /**
     * 解密数据
     */
    fun decrypt(original: String?): String?

    /**
     * 获取加解密字段名称
     */
    fun getParamName(): String
}
  • IParser
/**
 * 数据解析器抽象接口
 *
 * @see [DefaultParser]
 * @author: FreddyChen
 * @date  : 2022/01/06 17:53
 * @email : freddychencsc@gmail.com
 */
interface IParser {
    fun<T> parse(url: String, data: String, type: Type): T
}
  • IApiService
/**
 * 统一的请求方式
 * @author: FreddyChen
 * @date  : 2022/01/07 11:08
 * @email : freddychencsc@gmail.com
 */
internal interface IApiService {

    /**
     * 异步GET请求
     * 无参
     */
    @GET
    suspend fun get(@Url function: String): String

    /**
     * 异步GET请求
     * 带参
     */
    @GET
    suspend fun get(@Url function: String, @QueryMap params: ArrayMap<String, Any?>): String

    /**
     * 异步POST请求
     * 无参
     */
    @POST
    suspend fun post(@Url function: String): String

    /**
     * 异步POST请求
     * 带参
     */
    @POST
    suspend fun post(@Url function: String, @Body body: RequestBody): String

    /**
     * 异步PUT请求
     * 无参
     */
    @PUT
    suspend fun put(@Url function: String): String

    /**
     * 异步PUT请求
     * 带参
     */
    @PUT
    suspend fun put(@Url function: String, @Body body: RequestBody): String

    /**
     * 异步DELETE请求
     * 无参
     */
    @DELETE
    suspend fun delete(@Url function: String): String

    /**
     * 异步DELETE请求
     * 带参
     */
    @DELETE
    suspend fun delete(@Url function: String, @QueryMap params: ArrayMap<String, Any?>): String

    /**
     * 同步GET请求
     * 无参
     */
    @GET
    fun syncGet(@Url function: String): Call<String>

    /**
     * 同步GET请求
     * 带参
     */
    @GET
    fun syncGet(@Url function: String, @QueryMap params: ArrayMap<String, Any?>): Call<String>

    /**
     * 同步POST请求
     * 无参
     */
    @POST
    fun syncPost(@Url function: String): Call<String>

    /**
     * 同步POST请求
     * 带参
     */
    @POST
    fun syncPost(@Url function: String, @Body body: RequestBody): Call<String>

    /**
     * 同步PUT请求
     * 无参
     */
    @PUT
    fun syncPut(@Url function: String): Call<String>

    /**
     * 同步PUT请求
     * 带参
     */
    @PUT
    fun syncPut(@Url function: String, @Body body: RequestBody): Call<String>

    /**
     * 同步DELETE请求
     * 无参
     */
    @DELETE
    fun syncDelete(@Url function: String): Call<String>

    /**
     * 同步DELETE请求
     * 带参
     */
    @DELETE
    fun syncDelete(@Url function: String, @QueryMap params: ArrayMap<String, Any?>): Call<String>
}

使用方式

  1. 添加依赖
  • Java implementation "io.github.freddychen:shine-java:$lastest_version"
  • Kotlin implementation "io.github.freddychen:shine-kotlin:$lastest_version"

Note:最新版本可在maven central shine中找到。

  1. 初始化

使用Shine前进行初始化,建议放到Application#onCreate()。

val options = ShineOptions.Builder()
        .setLogEnable(true)
        .setLogTag("FreddyChen")
        .setBaseUrl("https://api.oick.cn/")
        .setParserCls(CustomParser1::class)
        .build()
ShineKit.init(options)

当然,初始化不是强制的,ShineOptions会有对应的默认值,默认值可参考参数及API说明#ShineOptions

  1. 使用
suspend fun fetchCatList(): ArrayList<Cat> {
    val options = RequestOptions.Builder()
        .setRequestMethod(RequestMethod.GET)
        .setBaseUrl("https://cat-fact.herokuapp.com/")
        .setFunction("facts/random?amount=2&animal_type=cat")
        .build()

    val type = object : TypeToken<ArrayList<Cat>>() {}.type
    return ShineKit.getRequestManager().request(
      options = options,
      type = type,
      parserCls = CustomParser1::class
    )
}

当然,Type及Parser参数传递我们可以利用Kotlin特性封装一个通用的请求方法,这些大家根据自己的业务情况来选择就好,下面提供一个示例:

/**
 * 异步请求
 */
suspend inline fun <reified T> request(
    requestMethod: RequestMethod,
    baseUrl: String = "https://api.oick.cn/",
    function: String,
    headers: ArrayMap<String, Any?>? = null,
    params: ArrayMap<String, Any?>? = null,
    contentType: String = NetworkConfig.DEFAULT_CONTENT_TYPE,
    parserCls: KClass<out IParser> = CustomParser1::class,
    cipherCls: KClass<out ICipher>? = null
 ): T {
    val optionsBuilder = RequestOptions.Builder()
        .setRequestMethod(requestMethod)
        .setBaseUrl(baseUrl)
        .setFunction(function)
        .setContentType(contentType)

    if (!headers.isNullOrEmpty()) {
        optionsBuilder.setHeaders(headers)
    }

    if (!params.isNullOrEmpty()) {
        optionsBuilder.setParams(params)
    }

    return ShineKit.getRequestManager()
        .request(optionsBuilder.build(), object : TypeToken<T>() {}.type, parserCls, cipherCls)
 }

这样的话,上面的请求可以简化为:

suspend fun fetchCatList(): ArrayList<Cat> {
    return request(
        requestMethod = RequestMethod.GET,
        baseUrl = "https://cat-fact.herokuapp.com/",
        function = "facts/random?amount=2&animal_type=cat",
    )
}
  1. 示例
  • 获取历史列表数据
服务器域名 接口地址 参数 返回数据结构 备注
api.oick.cn/ lishi/api.php / code、day、result /

例:

{
    "code":"1",
    "day":"01/ 17",
    "result":[
        {
            "date":"395年01月17日",
            "title":"罗马帝国分裂为西罗马帝国和东罗马帝国"
        }
    ]
}

调用方式:

suspend fun fetchHistoryList(): ArrayList<History> {
    return request(
        requestMethod = RequestMethod.POST,
        function = "lishi/api.php",
    )
}
  • 获取新闻列表数据
服务器域名 接口地址 参数 返回数据结构 备注
is.snssdk.com/ api/news/feed/v51/ / message、data /

例:

{
    "message":"success",
    "data":[
        {
            "content":"test"
        }
    ]
}

调用方式:

suspend fun fetchJournalismList(): ArrayList<Journalism> {
    return request(
        requestMethod = RequestMethod.GET,
        baseUrl = "https://is.snssdk.com/",
        function = "api/news/feed/v51/",
        parserCls = CustomParser2::class,
    )
}

Note:如有业务需求使用同步请求方式,只需要把request()方法改成syncRequest()方法即可

版本记录

版本号 修改时间 版本说明
0.0.7 2022.01.16 首次提交

免费开放的Api

提供两个免费开放Api平台给大家,方便测试:

写在最后

终于写完了,网络请求基本是每个Android应用必须用到的组件,Shine为平时工作中的积累,也算是一种总结,希望对大家有所帮助。由于水平有限,也许Shine并不是最好的封装方式,开源这个项目,旨在起到抛砖引玉的作用,欢迎大家star和fork,让我们为Android开发共同贡献一份力量。

GitHub地址:

ftd2

转载请注明:Android开发中文站 » Shine——更简单的Android网络请求库封装

您必须 登录 才能发表评论!