2

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

 2 years ago
source link: http://www.androidchina.net/12225.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Shine是什么?

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

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

Shine能做什么?

  • 支持的请求
    • 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 : [email protected]
 */
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 : [email protected]
 */
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 : [email protected]
 */
interface IParser {
    fun<T> parse(url: String, data: String, type: Type): T
}
  • IApiService
/**
 * 统一的请求方式
 * @author: FreddyChen
 * @date  : 2022/01/07 11:08
 * @email : [email protected]
 */
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>
}
  • Java implementation "io.github.freddychen:shine-java:$lastest_version"
  • Kotlin implementation "io.github.freddychen:shine-kotlin:$lastest_version"

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

使用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

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",
    )
}
  • 获取历史列表数据

服务器域名 接口地址 参数 返回数据结构 备注

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地址:


Recommend

  • 264

    写在前面:自从Vue2.0推荐大家使用 axios 开始,axios 被越来越多的人所了解。使用axios发起一个请求对大家来说是比较简单的事情,但是axios没有进行封装复用,项目越来越大,引起的代码冗余。就会非常麻烦的一件事。所以本文会详细的跟大家介绍,如

  • 96

    axios请求封装和异常统一处理 当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,...

  • 58
    • 掘金 juejin.im 5 years ago
    • Cache

    vue中axios请求的封装

    vue中axios请求的封装 axios Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中, 也是vue官方推荐使用的http库;封装axios,一方面为了以后维护方便,另一方面也可以对请求进行自定义处理 安装 n

  • 29
    • www.tuicool.com 4 years ago
    • Cache

    Vue项目中使用Axios封装http请求

    使用axios可以统一做请求响应拦截,例如请求响应时我们拦截响应信息,判断状态码,从而弹出报错信息。请求超时的时候断开请求,还可以很方便地使用then或者catch来处理请求。

  • 15

    二次封装 requests,构造通用的请求函数 作者

  • 4

    [本文结构] 一款封装HttpURLConnection实现的简单的网络请求的事例,提供了对应的apk和源码以及调用事例。暂时放上源码,后续提供代码介绍。 github:...

  • 7

    golang封装解析请求参数(使用不同的请求头) tonnyzhang · 大约2小时之前 · 26 次点击 · 预计阅读时间 1 分钟 · 大约8小时之前 开始浏览    ...

  • 9

     不是吧,不是吧,原来真的有人都2021年了,连TypeScript都没听说过吧?在项目中使用TypeScript虽然短期内会增加一些开发成本,但是对于其需要长期维护的项目,TypeScript能够减少其维护成本,使用TypeScript增加了代码的可读性和可维护性,且拥有较为活跃的社...

  • 7
    • blogbo.org 3 years ago
    • Cache

    iOS网络请求类的简单封装

    四公子的剑This is my site, welcome you!iOS网络请求类的简单封装 这篇文章主要讲解一下使用AFNetWorking(以下简称AFN)来自定义一个完整的网络请求类,来进行常用的网络请求后台...

  • 8

    Android 封装工具类问题,请求大佬解答 V2EX  ›  Android Android 封装工具类问题,请求大佬解答  

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK