封装 retrofit 网络请求工具

本文长话短说,不啰嗦

项目地址:BW_Libs

这个测试接口,我试的时候时不时的没有数据,别的关键字我也没试

思考

我看很多人在做 http 网络请求工具时,都是把业务层逻辑和 lib 层逻辑放在一起了,这样不方便以后更换网络框架

想 retrofit 对象,okhttpclient 对象这些是 lib 层的代码

但是像添加 head 的 intercepter 拦截器,公共业务code处理这些就应该放在业务层了,应该是和 lib 层分离的

至于 retrofit 的注解网络接口,我个人倾向于使用公共的 get、post 请求,这样不会一改起来,整个 app 整个改,改动的地方会少很多

现在的框架越做越好,但是不得不承认的是,实现仙童功能的框架之间差异性越来越大,这给我们造成了另一个烦恼,如何对开源框架再封装

基于以上几点,我开始构建我的网络 lib


功能封层

我的实现很简单的,代码很少,主要看个思路吧,不喜欢的请喷我吧~


lib 层:

  • CommonService 提供统一的 get、post 请求,另外也支持具体的 retrofit interface
  • ApiException 自定义的 exception 对象
  • ErrorInterceptor 错误嘛拦截器,用于统一处理网络状态码,返回适合本公司的 message 提示文字,注意这里处理的不是业务 code
  • HttpManager 网络工具,单例对外提供读服务

业务层:

  • BlueService 用于测试的一个 retrofit interface
  • BaseResponse<T> 公共数据返回类型
  • CommonHttpFunctionByBaseResponse 继承 Function 用于统一处理公司业务,比如 T 票,用户异地登录,踢掉当前使用者
  • BookResponse 非标准数据类型,这是因为测试接口返回不是上面的标准数据类型
  • BookRepositroy 摸个业务的公共数据层

这里面好些都是测试用的,有用的没几个,很容易理解


lib 层思路

1. 先来看 CommonService

CommonService 提供统一的 get、post 访问,也支持具体的 retrofit interface

public interface CommonService {

    @GET("{xxxUrl}")
    Observable<ResponseBody> getMethod(@Path("xxxUrl") String url, @QueryMap Map<String, String> options);

    @GET("{xxxUrl}")
    Observable<ResponseBody> getMethod(@Path("xxxUrl") String url);

    @FormUrlEncoded
    @POST("{xxxUrl}")
    Observable<ResponseBody> postMethod(@Path("xxxUrl") String url, @FieldMap Map<String, String> options);

}

统一的 get、post 接口无法使用泛型,所以这里我返回 ResponseBody, response 网络响应原生类,这个 ResponseBody 是 okhttp3 的

2. ErrorInterceptor

我不知道大家需不需要这个处理,我司这里不让显示框架提示的文字,需要我们自己根据网络相应码自己抛一个 exception 给最后的 onError 处理

class ErrorInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        var request = chain.request()
        val response = chain.proceed(request)

        if (401 == response.code()) {
            throw ApiException("身份验证错误!")
        } else if (403 == response.code()) {
            throw ApiException("禁止访问!")
        } else if (404 == response.code()) {
            throw ApiException("链接错误")
        } else if (408 == response.code()) {
            throw ApiException("请求超时!")
        } else if (503 == response.code()) {
            throw ApiException("服务器升级中!")
        } else if (500 == response.code()) {
            throw ApiException("服务器内部错误!")
        }
        return response
    }
}

我这里没写太多,但是我查了查网络错误吗有很多,若是用的话,大家还是去 baidu 写全吧~

3. HttpManager
  1. HttpManager 肯定是单例的啦,然后 HttpManager 里面有一个通用的 okHttpClient、retrofit 对象,这种情况只能应对一个 baseUrl,若是您的单位的 app 有多个 baseUrl ,那么请创建 map 来保存 baseUrl 对应 okHttpClient、retrofit

  2. HttpManager 对外提供 init 初始化方法,我没有对 okHttpClient 对象的 build 配置项再做 build 了,okHttpClient 的 build 配置项是在太多了,init 初始化中直接由外接传递进来一个 okHttpClient.build,这样最省事,虽然会造成一部分耦合,未来换框架会改,但是不是方便我们当下使用嘛,而且改的地方大家也都知道,就集中在这一处,也好改,算是代码封装和现实的拖鞋吧

剩下的没啥好说的了,都很简单,大家一看便知

class HttpManager {

    lateinit private var okHttpClient: OkHttpClient
    lateinit private var retrofit: Retrofit
    lateinit var baseUrl: String

    companion object {

        var connectTimeout: Long = 10 * 1000
        var readTimeout: Long = 10 * 1000
        var writeTimeout: Long = 10 * 1000

        var instance: HttpManager = HttpManager()
    }

    /**
     * 初始化网络数据,使用 OkHttpClient.Builder 传入主要参数到 ohkttp 对象中
     */
    fun init(baseUrl: String, builder: OkHttpClient.Builder?) {

        if (builder == null) {
            okHttpClient = OkHttpClient.Builder()
                    // 超时时间
                    .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                    .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
                    .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
                    // 网络相应 code 码处理,不含 app 业务 code 处理
                    .addInterceptor(ErrorInterceptor())
                    .build()
        } else {
            okHttpClient = builder
                    .addInterceptor(ErrorInterceptor())
                    .build()
        }

        retrofit = Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
    }

    /**
     * 标准 get 请求
     */
    fun get(url: String, options: Map<String, String>): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).getMethod(url, options).subscribeOn(Schedulers.io())
    }

    /**
     * 标准 get 请求
     */
    fun get(url: String): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).getMethod(url).subscribeOn(Schedulers.io())
    }

    /**
     * 标准 post 请求
     */
    fun post(url: String, options: Map<String, String>): Observable<ResponseBody> {
        return retrofit.create(CommonService::class.java).postMethod(url, options).subscribeOn(Schedulers.io())
    }

    /**
     * 支持用户使用自己定义的 RetrofitService,而非公共的 RetrofitService
     */
    fun <S> createRetrofitService(service: Class<S>): S {
        return retrofit.create(service)
    }

}

业务层思路

业务层没啥好说的了,除了拿来直接用的,剩下的值得我们关注的点就是怎么统一公共处理,思路有5:

  1. 写在 intercepter 拦截器里,这种思路太死了,有的接口不要处理某些 code ,还有若是统一处理逻辑要是和页面联系很紧密的话,写在 intercepter 拦截器里我们没法和页面交互
  2. 写一个统一的 function,再网络请求是同一添加,flatMap rxjava 操作符大家都知道吧
  3. 就是最原始的写法了,每个接口都写一遍,复制粘贴就行,缺点就是有改动太麻烦,改的地方很多
  4. 仿照 rxjava 提供的数据转换器思路,自定义一个数据转换器,在其中加入处理公共业务 code 的逻辑,这个思路缺点还是不够灵活,有的接口不需要处理某些 code 怎么办,错误逻辑需要页面配合怎么办,但是从代码封装的角度看,这块值得我们自己联系,就算不用也是值得自己写写,找找感觉的
Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create())

详细请参考:

  1. rxjava 种所有的错误处理我们都是在 最后的 onError 中处理,这时的 exception 有可能是系统抛给我们的,也有可能是我们自己抛出的业务错误,这块如何统一处理呢?思路就是我们继承 Subscriber<T> 自己写一个 BaseSubscriber<T>,在 onError 中统一处理下错误,比如下面,我们统一处理下错误 message 应该显示的是什么
//辅助处理异常
public class ApiErrorHelper {
    public static void handleCommonError(Context context, Throwable e) {
        if (e instanceof HttpException) {
            Toast.makeText(context, "服务暂不可用", Toast.LENGTH_SHORT).show();
        } else if (e instanceof IOException) {
              Toast.makeText(context, "连接失败", Toast.LENGTH_SHORT).show();
        } else if (e instanceof ApiException) {
           //ApiException处理
        } else {
             Toast.makeText(context, "未知错误", Toast.LENGTH_SHORT).show();
        }
    }
}

我是愿意使用第二种方法的,写一个统一的 function 处理公共业务

public class CommonHttpFunctionByBaseResponse<T> implements Function<BaseResponse<T>, T> {

    @Override
    public T apply(BaseResponse<T> baseResponse) throws Exception {

        if (!baseResponse.isSuccess()) {
            // 有特殊处理,可以在这里进行,比如 T 票,并不是所有的接口都要相应 Token 被 T 的问题
            // 这里在具体的 response 类里自行判断是不是要添加处理,也是为了灵活一些
            Observable.error(new Exception(baseResponse.getMessage()));
        }
        return baseResponse.getData();
    }
}

但是我在这里遇到过不去的问题了,gson 这块我不知道怎么写了,汗一个,学艺不精啊,就是拿到:接口返回 ResponseBody 对象,responseBody.string() 拿到 json 字符串,怎么通过传泛型 T 来转换成 BaseResponse<T> 类型我就不会写了。无奈 gson 这块我只能每个接口都写一遍了了。

但是对于 BaseResponse.code 我还是封装了 Function 对象统一处理


最后大家看一下如何使用

  1. activity 层
        getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                Disposable disposable = new BookRepositroy()
                        .get("小王子", "", "0", "20")
                        .subscribe(
                                new Consumer<BookResponse>() {
                                    @Override
                                    public void accept(BookResponse bookResponse) throws Exception {
                                        List<BookResponse.Book> books = bookResponse.getBooks();
                                        adapter.refreshData(books);
                                    }
                                },
                                new Consumer<Throwable>() {
                                    @Override
                                    public void accept(Throwable throwable) throws Exception {
                                        // ApiException.getErrorInfo(throwable) 获取统一错误信息
                                        Log.d("AA", "错误:" + ApiException.getErrorInfo(throwable));
                                    }
                                }
                        );
            }
  1. 数据层
public class BookRepositroy {

    public static final String URL_BOOK_LIST = "book/search";

    public Observable<BookResponse> get(String title, String tag, String startCount, String wantCount) {

        Map<String, String> map = new HashMap<>();
        map.put("q", title);
        map.put("tag", tag);
        map.put("start", startCount);
        map.put("count", wantCount);

        return HttpManager.Companion.getInstance().get(URL_BOOK_LIST, map)
                .map(new Function<ResponseBody, BookResponse>() {
                    @Override
                    public BookResponse apply(ResponseBody responseBody) throws Exception {
                        BookResponse bookResponse = null;
                        try {
                            bookResponse = new Gson().fromJson(responseBody.string(), BookResponse.class);
                        } catch (Exception e) {
                            Observable.error(new ApiException("数据异常"));
                        }
                        return bookResponse;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread());
    }
}

  1. 初始化网络配置

这里我演示了下添加 head 和 我司的 MD5 加密

    fun initHttp() {

        var baseUrl = "https://api.douban.com/v2/"

        /**
         * 请求头拦截器
         * 1. 可以判断网络地址是否需要特殊处理
         * if (s.contains("androidxx")) {
        request = request.newBuilder().url("http://www.androidxx.cn").build();
        }
         */
        var headInterceptor = object : Interceptor {
            override fun intercept(chain: Interceptor.Chain): Response {
                var request = chain.request()

                // 获取 post MD5 加密串
                var md5: String = ""
                var method: String = request.method()
                if (method.equals("post")) {
                    val requestBody = request.body()
                    if (requestBody is FormBody && requestBody.size() > 0) {
                        var json = JSONObject()
                        var formBody: FormBody = requestBody as FormBody
                        formBody.size()
                        for (index in 0..formBody.size()) {
                            json.put(formBody.encodedName(index), formBody.encodedValue(index))
                        }
                        md5 = json.toString()
                    }
                }

                // 添加请求头
                var requestBuilder: Request = request.newBuilder()
                        .addHeader("Connection", "AA")
                        .addHeader("token", "token-value")
                        .addHeader("MD5", md5)
                        .method(request.method(), request.body())
                        .build()
                return chain.proceed(requestBuilder);
            }
        }

        var httpBuild = OkHttpClient.Builder()
                .connectTimeout(HttpManager.connectTimeout, TimeUnit.MILLISECONDS)
                .readTimeout(HttpManager.readTimeout, TimeUnit.MILLISECONDS)
                .writeTimeout(HttpManager.writeTimeout, TimeUnit.MILLISECONDS)
//                .addInterceptor(headInterceptor)

        HttpManager.instance.init(baseUrl, httpBuild)
    }

大家对于 retrofit ,okhttp 的优秀应用

这里我只记录在做网路开发中大伙做过的有意思的处理

  1. 并发 token 的处理

这位兄弟的做法是利用了拦截器,在请求时判断 head 里面的 token 和当前存储的 token 一样不一样,不一样的话把新的 token 写进 head 再请求

判断 response 的 code 要是 token 过期的话,用 synchronized 同步代码块先锁死网络请求,然后启动一个申请新的 token 的网络操作,在结果回来后,刷新本地记录的 token ,再重新进行请求

我是不太赞同这样的做法的,太耗时了,用户在不知情的情况下可能要等待很久,另外申请新 token 请求要是不成功呢,怎么处理

  1. 图片上传
    图片上传看下面这个例子,知乎图片选择器 + uri 转 file + 鲁班压缩,这一套下来非常 nice
  1. 对 OKHttp 缓存的实战处理

这个还是值得看的,经过实战,有的点作者会指出来

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

推荐阅读更多精彩内容