【Android】用retrofit2和rxjava2搭建网络请求框架

一、准备工作

以前用Retrofit和RxJava搭建的网络请求框架问题比较多,使用起来也不方便,所以有时间以后,就想重新搭一套比较通用的框架,上传到JCenter,以后新开项目也可以直接用,所以诞生了这篇博客。首先,整理一下我能想到的需求:

1、需求:

(1)多baseurl

通常项目最少会有两套环境,一套线上环境,一套测试环境。不同环境使用的域名不同,目前我遇到的有两种情况,一种是打包时,根据不同的环境打的包使用的域名不同,另一种是调试时,在多个环境之间切换。

(2)可设置请求超时时间

需要可以设置超时时间和超时时间的单位,如果不设置超时时间和超时时间的单位,则有一个默认的超时时间和时间单位。

(3)添加拦截器

可以根据不同的需要在初始化时添加需要的拦截器。

(4)添加请求头

添加一个拦截器,在拦截器中实现添加请求头。

(5)实现可定制BaseResponse

给定一个默认的BaseResponse,同时在封装的Subscriber中进行类型判断,继承自BaseResponse的封装好状态判断等操作,不继承则需要自己进行更多的处理,甚至可以自己封装Subscriber。推荐封装自用的Subscriber,可以更好的符合自己的需求。

(6)BaseView应该有哪些东西?是否可以定制?

实现BaseView就得实现BaseView所有方法,所以需要通用性,从多个方面考虑后,发现只有加载具有通用性,因为个人觉得加载提示是请求接口时必不可少的东西。所以在BaseView中只封装了loading()方法。若有其他需求,则可定制自己的BaseView。

目前我能想到的需求只有这些,后边有时间可能会加入下载上传以及进度展示的封装,我的思路也可能存在问题,如果有问题,请各位大佬指正。

2、添加依赖

由于我是打算封装好以后上传到JCenter直接使用的,所以另开了一个项目,直接在项目的build.gradle中加入了以下依赖:

implementation 'io.reactivex.rxjava2:rxjava:2.1.0'
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'com.squareup.retrofit2:retrofit:2.2.0'
implementation 'com.squareup.retrofit2:converter-gson:2.2.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.6.0'
api 'com.orhanobut:logger:2.1.1'
api 'com.alibaba:fastjson:1.2.47'

二、实现

1、创建Retrofit帮助类

创建一个名为RetrofitHelper的类,主要用于实现OkHttpClient和Retrofit相关设置。通过一个建造者模式,来实现需求中的1-4条。以下代码为Builder类:

public static final class Builder {
    private OkHttpClient mOkHttpClient;
    private OkHttpClient.Builder mBuilder;
    private String mBaseUrl;

    private long mConnectTimeout;
    private TimeUnit mConnectTimeUnit;
    private long mReadTimeout;
    private TimeUnit mReadTimeUnit;
    private long mWriteTimeout;
    private TimeUnit mWriteTimeUnit;

    public Builder() {
        mBuilder = new OkHttpClient.Builder();
    }

    public Builder(OkHttpClient okHttpClient) {
        mOkHttpClient = okHttpClient;
    }

    public Builder(RetrofitHelper retrofitHelper) {
        mOkHttpClient = retrofitHelper.mOkHttpClient;
    }

    /**
     * 添加拦截器
     *
     * @param interceptor 拦截器
     * @return this
     */
    public Builder addInterceptor(Interceptor interceptor) {
        if (mOkHttpClient != null) {
            mBuilder = mOkHttpClient.newBuilder();
        }
        if (mBuilder != null) {
            mBuilder.addInterceptor(interceptor);
        }
        return this;
    }

    /**
     * 添加拦截器来实现动态添加多个请求头
     *
     * @param headers 需要添加的header
     * @return this
     */
    public Builder addHeaders(final HashMap<String, String> headers) {
        return addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Request.Builder builder = request.newBuilder();
                Set<String> keys = headers.keySet();
                if (keys.iterator().hasNext()) {
                    builder.addHeader(keys.iterator().next(), headers.get(keys.iterator().next()));
                }
                return chain.proceed(request);
            }
        });
    }

    /**
     * 添加单个请求头
     *
     * @param key   请求头key
     * @param value 请求头value
     * @return this
     */
    public Builder addHeader(String key, String value) {
        HashMap<String, String> headers = new HashMap<>(1);
        headers.put(key, value);
        return addHeaders(headers);
    }

    /**
     * 设置baseUrl
     *
     * @param baseUrl baseUrl
     * @return this
     */
    public Builder baseUrl(String baseUrl) {
        mBaseUrl = baseUrl;
        return this;
    }

    /**
     * 设定Url是否可以变化
     *
     * @return this
     */
    public Builder urlCanTrans() {
        return addInterceptor(HttpUrlInterceptor.getInstance());
    }

    /**
     * 连接超时时间{@link #connectTimeout(long, TimeUnit)}
     *
     * @param connectTimeout 超时时间
     * @return this
     */
    public Builder connectTimeout(long connectTimeout) {
        connectTimeout(connectTimeout, TimeUnit.MILLISECONDS);
        return this;
    }

    /**
     * 连接超时时间
     *
     * @param connectTimeout  超时时间
     * @param connectTimeUnit 超时时间单位
     * @return this
     */
    public Builder connectTimeout(long connectTimeout, TimeUnit connectTimeUnit) {
        mConnectTimeout = connectTimeout;
        mConnectTimeUnit = connectTimeUnit;
        return this;
    }

    /**
     * 读数据超时时间{@link #readTimeout(long, TimeUnit)}
     *
     * @param readTimeout 超时时间
     * @return this
     */
    public Builder readTimeout(long readTimeout) {
        readTimeout(readTimeout, TimeUnit.MILLISECONDS);
        return this;
    }

    /**
     * 读数据超时时间
     *
     * @param readTimeout  超时时间
     * @param readTimeUnit 超时时间单位
     * @return this
     */
    public Builder readTimeout(long readTimeout, TimeUnit readTimeUnit) {
        mReadTimeout = readTimeout;
        mReadTimeUnit = readTimeUnit;
        return this;
    }

    /**
     * 写数据超时时间{@link #writeTimeout(long, TimeUnit)}
     *
     * @param writeTimeout 超时时间
     * @return this
     */
    public Builder writeTimeout(long writeTimeout) {
        writeTimeout(writeTimeout, TimeUnit.MILLISECONDS);
        return this;
    }

    /**
     * 写数据超时时间
     *
     * @param writeTimeout  超时时间
     * @param writeTimeUnit 超时时间单位
     * @return this
     */
    public Builder writeTimeout(long writeTimeout, TimeUnit writeTimeUnit) {
        mWriteTimeout = writeTimeout;
        mWriteTimeUnit = writeTimeUnit;
        return this;
    }

    /**
     * 构造OkHttpClient
     */
    private void buildOkHttpClient() {
        if (mConnectTimeout != 0 && mConnectTimeUnit != null) {
            mBuilder.connectTimeout(mConnectTimeout, mConnectTimeUnit);
        } else {
            mBuilder.connectTimeout(TIME_OUT, TimeUnit.MILLISECONDS);
        }

        if (mReadTimeout != 0 && mReadTimeUnit != null) {
            mBuilder.readTimeout(mReadTimeout, mReadTimeUnit);
        } else {
            mBuilder.readTimeout(TIME_OUT, TimeUnit.MILLISECONDS);
        }

        if (mWriteTimeout != 0 && mWriteTimeUnit != null) {
            mBuilder.writeTimeout(mWriteTimeout, mWriteTimeUnit);
        } else {
            mBuilder.writeTimeout(TIME_OUT, TimeUnit.MILLISECONDS);
        }

        if (BuildConfig.DEBUG) {
            addInterceptor(new LoggingInterceptor());
        }
        mOkHttpClient = mBuilder.build();
    }

    /**
     * 创建Retrofit对象
     *
     * @return 创建好的Retrofit对象
     */
    public Retrofit build() {
        if (mBaseUrl == null) {
            throw new IllegalArgumentException("Illegal Url, You must have baseurl");
        }

        if (mOkHttpClient == null) {
            buildOkHttpClient();
        }
        return RetrofitHelper.getInstance(mBaseUrl, mOkHttpClient).createRetrofit();
    }
}

以下为各个方法的解析:
addInterceptor:不用多说,用于给OkHttpClient添加拦截器;
addHeaders和addHeader:使用addInterceptor方法来给请求分别添加多个和单个Header;
baseUrl:是用于给Retrofit的baseUrl赋值。
urlCanTrans:添加一个HttpUrlInterceptor,HttpUrlInterceptor是使用拦截器的方式修改baseurl。用于实现需求中的第一条。
timeout方法:用于给OkHttpClient设置超时时间。
buildOkHttpClient:设置OkHttpClient的各种值,在调试时,加入LoggingInterceptor,用于打印请求相关信息。
build:生成Retrofit对象,如果没有设定baseUrl,则会抛出异常。

最终创建一个Retrofit对象:

private Retrofit createRetrofit() {
    Retrofit.Builder builder = new Retrofit.Builder();
    builder.client(mOkHttpClient)
            .baseUrl(mBaseUrl)
            .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().create()))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()));
    return builder.build();
}

这里的ConverterFactory采用了GsonConverterFactory,因为现在基本都是用Json格式交互,所以直接默认使用了GsonConverterFactory。到这里,Retrofit帮助类就告一段落了,下一步使用工厂模式进一步封装,这一步封装主要是为了给HttpUrlInterceptor使用所封装的,具体细节在HttpUrlInterceptor时描述。

2、RetrofitFactory

这个类本可以不用封装,但是在使用拦截器转换baseUrl时,会将API类中的全路径接口的域名给替换掉,与Retrofit本身相悖,因为Retrofit中的API的全路径是不受baseUrl影响的,所以封装了这个类。以下抽出该类中的两个方法来简述一下:

private static Class mClass;

public <T> T createApi(String baseUrl, Class<T> clazz) {
    return createApi(baseUrl, clazz, false);
}

public <T> T createApi(String baseUrl, Class<T> clazz, boolean canTransUrl) {
    mClass = clazz;

    RetrofitHelper.Builder builder = new RetrofitHelper.Builder().baseUrl(baseUrl);

    if (canTransUrl) {
        builder.urlCanTrans();
    }

    Retrofit retrofit = builder.build();
    return retrofit.create(clazz);
}

其中这个mClass就是关键,在这个类中有个静态方法getClazz()返回mClass。上述的crateApi方法就是用RetrofitHelper来创建与clazz相对应的API对象。

3、HttpUrlInterceptor

这个类是用来切换baseUrl的关键,该类继承自Interceptor,是通过拦截请求,修改请求的域名来切换baseUrl,这里就有个问题,如何不修改API中的全路径接口,就需要用到RetrofitFactory中的mCalss了。

 private static List<String> getMethods(Class clazz) {
    if (clazz == null) {
        return null;
    }

    List<String> resultMethods = new ArrayList<>();
    Method[] methods = clazz.getMethods();
    for (int i = 0; i < methods.length; i++) {
        Method method = methods[i];
        Annotation[] annotations = method.getDeclaredAnnotations();
        for (int j = 0; j < annotations.length; j++) {
            Annotation annotation = annotations[j];
            if (annotation instanceof GET) {
                String value = ((GET) annotation).value();
                addUrl(resultMethods, value);
            } else if (annotation instanceof POST) {
                String value = ((POST) annotation).value();
                addUrl(resultMethods, value);
            }
        }
    }

    return resultMethods;
}

这个方法是通过反射,来获取API类中的全路径加入List中,用来判断是否需要替换域名。其中的value,就是GET和POST注解中的路径。

4、IBaseView接口

View接口主要是用于定义与UI交互的方法,IBaseView是只封装了loading这一方法,网络请求时有加载提示应该算是比较通用的,没有想到其他通用的方法,所以就只有一个loading方法,若有其他需要,只需要继承自IBaseView,自己封装方法即可。

public interface IBaseView {
    void loading();
}

5、IBasePresenter接口

public interface IBasePresenter<T extends IBaseView> {
    /**
     * 与View绑定
     * @param t 需要绑定的View
     */
    void attachView(T t);

    /**
     * 在Activity销毁时,与View解除绑定
     */
    void detachView();
}

IBaseView接口定义了两个方法,一个attachView,一个detachView,分别是用来与View绑定和解除绑定的。这两个方法可以在Activity基类中使用,这样就可以避免每次在Activity中都要使用这两个方法。

6、BasePresenter

这个类是Presenter基类,主要用于做一些公共的操作。代码如下:

public class BasePresenter<T extends IBaseView, V> implements IBasePresenter<T> {
    protected T mView;
    private CompositeDisposable mCompositeDisposable;
    protected V mApi;

    public BasePresenter() {
        mApi = getInstanceV();
    }

    @SuppressWarnings("unchecked")
    private V getInstanceV() {
        ParameterizedType superClass = (ParameterizedType) getClass().getGenericSuperclass();
        Class<V> type = (Class<V>) superClass.getActualTypeArguments()[1];
        try {
            return type.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected void addSubscriber(DisposableSubscriber subscriber) {
        if (mCompositeDisposable == null) {
            mCompositeDisposable = new CompositeDisposable();
        }
        mCompositeDisposable.add(subscriber);
    }

    private void unSubscribe() {
        if (mCompositeDisposable != null) {
            mCompositeDisposable.clear();
        }
    }

    @Override
    public void attachView(T view) {
        mView = view;
    }

    @Override
    public void detachView() {
        mView = null;
        unSubscribe();
    }

}

上述代码中的泛型V,是MVP中的M层,用于处理数据和进行数据库操作等。以代码为例:

public Flowable<List<User>> findAllUser() {
        return api.findAllUser();
    }

上述代码为M层的类,命名为DataManage类中定义的一个方法,用于获取全部的用户。这个DataManager类,就是BasePresenter中的泛型V,我们在构造函数中,直接实例化了该泛型,所以在子类Presenter中,可以直接使用mApi来调用DataManager中类,而不需要再去子类的构造函数中创建DataManager的实例。下边的代码为子类Presenter的调用请求全部用户的代码:

@Override
public void findAllUser() {
    addSubscriber(mApi
            .findAllUser()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeWith(new CommonSubscriber<List<User>>(mView) {
                @Override
                public void success(List<User> allUser) {
                    super.success(allUser);
                    mView.findAllUserSuccess(allUser);
                }

                @Override
                protected void failure(int exceptionCode, String errorMsg) {
                    super.failure(exceptionCode, errorMsg);
                    mView.findAllUserFail(errorMsg);
                }

                @Override
                protected void dealData(List<User> allUser) {
                    super.dealData(allUser);
                    mView.findAllUserSuccess(allUser);
                }
            }));
}

以上就是部分关键点代码。

三、总结

描述时总感觉很吃力,想要表达的意思描述不出来,所以贴了大段的代码,但是感觉也没有突出重点,可能是因为我的思路比较乱,没有一个总体的框架,后边我会写一例子来更好的理一下思路。另:该库已经上传到JCenter,使用时直接:implementation 'com.wanggle.network:network:1.1.6'即可。感觉基础还是比较薄弱,下一步计划就是分析一些源码,以此来提高自己。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • 这是一部由小说改篇的影片——《雾都孤儿》,它讲述了一个小男孩孤儿,从大落到大起的种种经历,通过这个影片可以看出,小...
    轻舟陌客阅读 534评论 0 1
  • 爱与艾草,每到端午节,人们都会买上几株艾草、放置家里,不仅可以驱蚊、还可享受艾草的清香,在经过太阳的照晒后、艾草就...
    AA日月明阅读 850评论 1 0