看我开发干货集中营App(三)~Retroft2+RxJava封装使用

个人原因,此系列更新已停止,抱歉。

上一篇《看我开发干货集中营App(二) ~ APP初始化》中讲述了此APP所依赖的一些第三方库,包括Retrofit2,Okhttp3,RxJava,Gilde等,对于使用第三方依赖库,我自己一直都秉承着封装一层在使用的原则。这样做的好处除了方便实际使用外,就是万一需要替换对应依赖库不用满世界去修改代码,只需要在封装入口处修改就好了。关于使用第三方开源项目,大家可以参考下这篇文字:如何正确使用开源项目?

接下来介绍下上面几个库的基本封装使用,提醒下以下内容将会基于你已经了解过对应的开源项目,而且知道如何使用,所以如果还未了解过相关库的请自行Google了解一番再回来看哦。

OK,先预览下《干货精选》的目录结构:


目录结构

关于Android App目录结构,个人比较推荐按模块分类。直接看看包名下的目录:

  • commonui: 通用UI封装类存放目录
  • core: 存放底层核心的封装类,如retrofit,rxjava等
  • modules: 按模块存放文件目录,主要是mvp开发模式的文件,如Google Todo-Mvp分支

Retrofit2封装

创建工具类:core/retofit/RetrofitHelper

RetrofitHelper特性:

  1. 单例形式创建Retrofit实例;
  2. 使用okhttp3作为请求客户端;
  3. 使用gson作为数据转换器;
  4. 使用RxJava优化异步请求流程;
  5. 开启数据缓存,无网络时可从缓存读取数据;
  6. 辅助类静态方法获取Retrofit Service实例。

节省篇幅,上代码看注释:

public class RetrofitHelper {

    private volatile static Retrofit retrofitInstance = null;

    /**
     * 创建Retrofit请求Api
     * @param clazz   Retrofit Api接口
     * @return api实例
     */
    public static <T> T createApi(Class<T> clazz){
        return getInstance().create(clazz);
    }


    // ===============================================================
    // private methods =================================================
    /**
     * 获取Retrofit实例
     * @return Retrofit
     */
    private static Retrofit getInstance(){
        if(null == retrofitInstance){
            synchronized (Retrofit.class){
                if(null == retrofitInstance){ // 双重检验锁,仅第一次调用时实例化
                    retrofitInstance = new Retrofit.Builder()
                            // baseUrl总是以/结束,@URL不要以/开头
                            .baseUrl(BuildConfig.API_SERVER_URL)
                            // 使用OkHttp Client
                            .client(buildOKHttpClient())
                            // 集成RxJava处理
                            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                            // 集成Gson转换器
                            .addConverterFactory(buildGsonConverterFactory())
                            .build();
                }
            }
        }
        return retrofitInstance;
    }

    /**
     * 构建OkHttpClient
     * @return OkHttpClient
     */
    private static OkHttpClient buildOKHttpClient(){
        // 添加日志拦截器,非debug模式不打印任何日志
        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
        loggingInterceptor.setLevel(AppUtil.isDev() ? HttpLoggingInterceptor.Level.HEADERS : HttpLoggingInterceptor.Level.NONE) ;

        return new OkHttpClient.Builder()
                .addInterceptor(loggingInterceptor)                       // 添加日志拦截器
                //.addInterceptor(buildTokenInterceptor())                // 添加token拦截器
                .addNetworkInterceptor(buildCacheInterceptor())           // 添加网络缓存拦截器
                .cache(getCache())                                        // 设置缓存文件
                .retryOnConnectionFailure(true)                           // 自动重连
                .connectTimeout(15, TimeUnit.SECONDS)                     // 15秒连接超时
                .readTimeout(20, TimeUnit.SECONDS)                        // 20秒读取超时
                .writeTimeout(20, TimeUnit.SECONDS)                       // 20秒写入超时
                .build();
    }

    /**
     * 获取缓存对象
     * @return Cache
     */
    private static Cache getCache(){
        // 获取缓存目标,SD卡
        File cacheFile = new File(AppUtil.getContext().getCacheDir(), ResourceUtil.getString(R.string.app_name_en));
        // 创建缓存对象,最大缓存50m
        return new Cache(cacheFile, 1024*1024*20);
    }

    /**
     * 构建缓存拦截器
     * @return Interceptor
     */
    private static Interceptor buildCacheInterceptor(){
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                // 无网络连接时请求从缓存中读取
                if (!AppUtil.isNetworkConnected()) {
                    request = request.newBuilder()
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                // 响应内容处理
                // 在线时缓存5分钟
                // 离线时缓存4周
                Response response = chain.proceed(request);
                if (AppUtil.isNetworkConnected()) {
                    int maxAge = 300;
                    response.newBuilder()
                            .header("Cache-Control", "public, max-age=" + maxAge)
                            .removeHeader("Pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                            .build();
                }else {
                    // 无网络时,设置超时为4周
                    int maxStale = 60 * 60 * 24 * 28;
                    response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                            .removeHeader("Pragma")
                            .build();
                }
                return response;
            }
        };
    }

    /**
     * 构建GSON转换器
     * @return GsonConverterFactory
     */
    private static GsonConverterFactory buildGsonConverterFactory(){
        GsonBuilder builder = new GsonBuilder();
        builder.setLenient();

        // 注册类型转换适配器
        builder.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
            @Override
            public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                return null == json ? null : new Date(json.getAsLong());
            }
        });

        Gson gson = builder.create();
        return GsonConverterFactory.create(gson);
    }
}

其中有几点需要说明下:

  1. AppUtil是另外一个工具类,封装了APP常用的一些方法,如判断网络状态,获取App版本、缓存路径等。
  2. API_SERVER_URL 是定义在 build.gradle 中的一个常量(干货集中营api基本URL),实际产品研发中可以使用同样方法定义不同开发环境(本地开发、测试、正式环境)的服务器地址。
  3. 缓存设置如果服务器响应Header不支持缓存,貌似缓存不起作用,欢迎大家帮忙测试。

使用方法:

假设有Retrofit Service Api如下:

public interface ArticleServiceApi {

    @GET("day/{year}/{month}/{day}")
    Observable<List<Article>> fetchByDate(@Path("year") String year, @Path("month") String month,
                                    @Path("day") String day);
}

执行api请求如下:

RetrofitHelper.createApi(ArticleServiceApi.class)
    .fetchByDate("2016", "09", "17")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<List<Article>>() {
        @Override
        public void onCompleted() {}

        @Override
        public void onError(Throwable e) {}

        @Override
        public void onNext(List<Article> articles) {}
    });

是不是比原始的请求简单不少。接下来对RxJava封装一层,使上面的请求更加简单点。

RxJava封装

再Retrofit中使用RxJava,需要封装的地方仅有2个地方,

  1. 切换线程操作
  2. 响应结果处理:onComplete()方法可选,通用错误响应,保留onSuccess()供用户调用

从干货集中营API返回结果可知,响应格式如下:

{
    error: false,
    results: [object, object]
}

// 或者

{
    error: false,
    category: [String,String...],
    results: {
        "Android": [{}, {}],
        "IOS": [{}, {}]
    }
}

为此,首先创建响应结果的实体基类 core/response/ResponseData

public class ResponseData {

    private boolean error;           // 是否请求错误

    public boolean isError() {
        return error;
    }
    public void setError(boolean error) {
        this.error = error;
    }
}

然后建立干货的实体类: modules/articles/entity/Article,以及API响应结果实体类modules/articles/entity/DailyArticles

基于以上响应实体类,我们创建RxJava订阅者的封装基类:core/rx/RxSubscriber

public abstract class RxSubscriber<T> extends Subscriber<T> {

    @Override
    public void onCompleted() {
        // 忽略操作,需要可覆写该方法
    }

    @Override
    public void onError(Throwable e) {
        if(e instanceof HttpException){
            String errNetwork = ResourceUtil.getString(R.string.err_network);
            LogUtil.error(e, "onError: " + errNetwork);
            ToastUtil.show(errNetwork);
        }

        // TODO: 处理其它通用错误
        // 覆写此方法自行处理异常,通用异常使用super.onError(e)保留
        e.printStackTrace();
    }

    @Override
    public void onNext(T t) {
        if(t instanceof ResponseData){
            ResponseData response = (ResponseData) t;
            // 判断是否请求错误,出错直接转到onError()
            if(response.isError()){
                Throwable e = new Throwable(ResourceUtil.getString(R.string.err_default));
                this.onError(e);
                return;
            }
        }
        onSuccess(t);
    }

    /**
     * 请求成功回调
     * @param t 最终响应结果
     */
     public abstract void onSuccess(T t);
}

这里封装并没有做太多处理,主要是对公共的异常做了一次处理,保留onSuccess()更多关注业务处理。这样封装感觉还不是很好,比如错误处理可以通过事件方式传递给View等,不过因为暂时对Android研究不深,还没有更好的想法。

接下来,对线程切换操作再做一个封装:core/rx/RxJavaHelper

对线程切换的封装需要用到RxJava的 compose() 方法,如果不太了解它的使用,可以参考下:给 Android 开发者的 RxJava 详解

public class RxJavaHelper {

    /**
     * 切换线程操作
     * @return Observable转换器
     */
    public static <T> Observable.Transformer<T, T> observeOnMainThread() {
        return new Observable.Transformer<T, T>() {
            @Override
            public Observable<T> call(Observable<T> tObservable) {
                return tObservable
                        .subscribeOn(Schedulers.io())                       // 在io线程中请求
                        .observeOn(AndroidSchedulers.mainThread());         // 请求完成后返回主线程处理
            }
        };
    }
}

封装基本完毕,再来看看请求api代码:

RetrofitHelper.createApi(ArticleServiceApi.class)
        .fetchByDate("2016", "09", "01")
        .compose(RxJavaHelper.<DailyArticles>observeOnMainThread())
        .subscribe(new RxSubscriber<DailyArticles>() {
            @Override
            public void onSuccess(DailyArticles dailyArticles) {
                LogUtil.debug("onSuccess: " + dailyArticles.getCategory().toString());
            }
        });

是不是有所改善~~

来看看请求结果:

D/OkHttp: --> GET http://gank.io/api/day/2016/09/01 http/1.1
D/OkHttp: --> END GET
D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
                  [ 09-21 20:50:57.557  2870: 2870 D/         ]
                  HostConnection::get() New Host Connection established 0xa230c570, tid 2870
                  [ 09-21 20:50:57.625  2870: 2918 D/         ]
                  HostConnection::get() New Host Connection established 0xae427510, tid 2918
I/OpenGLRenderer: Initialized EGL, version 1.4
D/OkHttp: <-- 200 OK http://gank.io/api/day/2016/09/01 (250ms)
D/OkHttp: Server: nginx/1.4.6 (Ubuntu)
D/OkHttp: Date: Wed, 21 Sep 2016 12:50:57 GMT
D/OkHttp: Content-Type: application/json
D/OkHttp: Content-Length: 4945
D/OkHttp: Connection: keep-alive
D/OkHttp: <-- END HTTP
D/GankEssence: ╔════════════════════════════════════════════════════════════════════════════════════════
D/GankEssence: ║ RxSubscriber.onNext  (RxSubscriber.java:51)
D/GankEssence: ║    MainActivity$1.onSuccess  (MainActivity.java:23)
D/GankEssence: ╟────────────────────────────────────────────────────────────────────────────────────────
D/GankEssence: ║ onSuccess: [瞎推荐, Android, 福利, 休息视频, iOS]
D/GankEssence: ╚════════════════════════════════════════════════════════════════════════════════════════

OK,还可以,Retrofit2 + RxJava 封装就暂时这样了。如果使用过程有什么问题再修改修改!!

本来还想再把Logger以及Glide的封装再说说,不过发现篇幅好长啊,而且Glide本身应实在够好了,不需要太多封装,只需封装个工具类方便我们自己使用就好了,Logger也是一样。大家有需要可以直接看看源码:ImageUtilLogUtil

本篇文章主要是简单说了第三方开源库的封装使用,其它的没说。对于Activity、Fragment等各类View组件使用,后面肯定会有封装一些适当的基类,特别是RecyclerView,会在后面开发过程提到的。


预告,下一篇《看我开发干货集中营App(四)~ 首页开发》将开始构建APP应用,功能点有RecyclerView使用、下拉刷新、上拉加载更多、HTML内容的解析等等。

Tags: 看我开发 Android开发 干货集中营 开放api Retroft2封装 RxJava封装 Glide封装

系列文章:

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

推荐阅读更多精彩内容