RxJava+Retrofit框架Demo(一)

从年前一两个月开始,就开始慢慢接触RxJava+Retrofit,针对以往开发中遇到的情况,慢慢写了一个框架Demo。文章不在进行入门介绍,需要了解的同学,可以查看笔者总结的文章RxJavaRetrofit

分割Response

一般来说,网络请求结果包括以下信息:
{ "message": "操作成功", "code": "1", "object": {} }
我们可以定义一个对象Response<T>,其中泛型T来表示object,可能是数组,也可能是对象。code为1(或者其他值,和后台商议)表示接口调用成功,如:登录成功,注册成功等;code为其他值,则表示失败,如登录失败等,此时message便返回对应的错误信息,如密码错误等。
如果返回结果为Response<T>,则每次网络请求都要判断接口是否调用成功,比较麻烦,我们希望的是:如果接口调用成功,返回泛型T,即object;如果调用失败,则返回codemessage信息。因此,需要对返回结果进行分割处理。
分割操作代码如下:

/**
 * 对网络接口返回的Response进行分割操作
 *
 * @param response
 * @param <T>
 * @return
 */
public <T> Observable<T> flatResponse(final Response<T> response) {
    return Observable.create(new Observable.OnSubscribe<T>() {

        @Override
        public void call(Subscriber<? super T> subscriber) {
            if (response.isSuccess()) {
                if (!subscriber.isUnsubscribed()) {
                    subscriber.onNext(response.object);
                }
            } else {
                if (!subscriber.isUnsubscribed()) {
                    subscriber.onError(new APIException(response.code, response.message));
                }
                return;
            }

            if (!subscriber.isUnsubscribed()) {
                subscriber.onCompleted();
            }

        }
    });
}

其中response.isSuccess()的代码如下:

 public boolean isSuccess() {
    return code.equals(Constant.OK);
}

通过以上代码,便可实现分割操作,这样每次返回结果都不用通过code来判断是否成功。

打印请求地址+参数

有些时候,为了方便调试,我们需要将网络请求的地址和参数log出来。由于Retrofit是基于OKHttp的,所以我们需要通过Interceptors来拦截OKHttp来log所需信息。
关于Interceptors,不再多说,直接附上代码。代码来自HttpLoggingInterceptor ,做了简化。

package com.sunflower.rxandroiddemo.utils;


import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.Platform;
import okio.Buffer;

/**
 * Created by Sunflower on 2016/1/12.
 */
public class HttpLoggingInterceptor implements Interceptor {
    private static final Charset UTF8 = Charset.forName("UTF-8");

    public enum Level {
        /**
         * No logs.
         */
        NONE,
        /**
         * Logs request and response lines.
         * <p/>
         * Example:
         * <pre>{@code
         * --> POST /greeting HTTP/1.1 (3-byte body)
         * <p/>
         * <-- HTTP/1.1 200 OK (22ms, 6-byte body)
         * }</pre>
         */
        BASIC,
        /**
         * Logs request and response lines and their respective headers.
         * <p/>
         * Example:
         * <pre>{@code
         * --> POST /greeting HTTP/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         * --> END POST
         * <p/>
         * <-- HTTP/1.1 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         * <-- END HTTP
         * }</pre>
         */
        HEADERS,
        /**
         * Logs request and response lines and their respective headers and bodies (if present).
         * <p/>
         * Example:
         * <pre>{@code
         * --> POST /greeting HTTP/1.1
         * Host: example.com
         * Content-Type: plain/text
         * Content-Length: 3
         * <p/>
         * Hi?
         * --> END GET
         * <p/>
         * <-- HTTP/1.1 200 OK (22ms)
         * Content-Type: plain/text
         * Content-Length: 6
         * <p/>
         * Hello!
         * <-- END HTTP
         * }</pre>
         */
        BODY
    }

    public interface Logger {
        void log(String message);

        /**
         * A {@link Logger} defaults output appropriate for the current platform.
         */
        Logger DEFAULT = new Logger() {
            @Override
            public void log(String message) {
                Platform.get().log(message);
            }
        };
    }

    public HttpLoggingInterceptor() {
        this(Logger.DEFAULT);
    }

    public HttpLoggingInterceptor(Logger logger) {
        this.logger = logger;
    }

    private final Logger logger;

    private volatile Level level = Level.BODY;

    /**
     * Change the level at which this interceptor logs.
     */
    public HttpLoggingInterceptor setLevel(Level level) {
        if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
        this.level = level;
        return this;
    }

    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Level level = this.level;

        Request request = chain.request();
        if (level == Level.NONE) {
            return chain.proceed(request);
        }

        boolean logBody = level == Level.BODY;
        boolean logHeaders = logBody || level == Level.HEADERS;

        RequestBody requestBody = request.body();
        boolean hasRequestBody = requestBody != null;

        String requestStartMessage = request.method() + ' ' + request.url();
        if (!logHeaders && hasRequestBody) {
            requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
        }
        logger.log(requestStartMessage);

        if (logHeaders) {

            if (!logBody || !hasRequestBody) {
                logger.log("--> END " + request.method());
            } else if (bodyEncoded(request.headers())) {
                logger.log("--> END " + request.method() + " (encoded body omitted)");
            } else if (request.body() instanceof MultipartBody) {
                //如果是MultipartBody,会log出一大推乱码的东东
            } else {
                Buffer buffer = new Buffer();
                requestBody.writeTo(buffer);

                Charset charset = UTF8;
                MediaType contentType = requestBody.contentType();
                if (contentType != null) {
                    contentType.charset(UTF8);
                }

                logger.log(buffer.readString(charset));

//                logger.log(request.method() + " (" + requestBody.contentLength() + "-byte body)");
            }
        }

        long startNs = System.nanoTime();
        Response response = chain.proceed(request);
        long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
        logger.log(response.code() + ' ' + response.message() + " (" + tookMs + "ms" + ')');

        return response;
    }

    private boolean bodyEncoded(Headers headers) {
        String contentEncoding = headers.get("Content-Encoding");
        return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
    }

    private static String protocol(Protocol protocol) {
        return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
    }
}

这样在初始化Retrofit时,我们可以通过以下代码来log请求地址+参数

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
    @Override
    public void log(String message) {
        Log.i("RxJava", message);
    }
});
OkHttpClient client = new OkHttpClient.Builder()
        //log请求参数
        .addInterceptor(interceptor)
        .build();

结果如下:


log网络请求地址+参数
log网络请求地址+参数

ApiWrapper封装类

对于一个APP来说,我们需要建立一个或者多个接口(我们先分析一个接口的情况,下文用APIService来替代),里面是相应的网络请求,然而不可能每次请求都初始化一个Retrofit对象,进而获得APIService对象,传入对应参数,进行网络请求,处理返回结果。
所以,首先可以新建RetrofitUtil类,用于初始化操作,网络结果分割操作等等;然后新建ApiWrapper封装类(继承自RetrofitUtil)。新建ApiWrapper封装类有什么好处呢?用代码来说明吧!
比说在APIService中有这样一个网络请求方法:

@FormUrlEncoded
@POST("api/common/msg.json")
Observable<Response<String>> getSmsCode(@Field("mobile") String mobile, @Field("appType") String appType);

该方法是用来获取短信验证码的,需要传入两个参数:手机号、app类型(医生端or孕妇端)
由于返回结果为验证码,即object字段为String类型,所以返回结果是Response<String>
通过ApiWrapper封装后,代码如下:

public Observable<String> getSmsCode(String mobile) {
    return getService().getSmsCode(mobile, "GRAVIDA")
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .flatMap(new Func1<Response<String>, Observable<String>>() {
                @Override
                public Observable<String> call(Response<String> stringResponse) {
                    return flatResponse(stringResponse);
                }
            });
}

其中getService()为父类RetrofitUtil中获取APIService对象的方法。
这样的话,在对应Activity中调用起来就很方便了

ApiWrapper wrapper = new ApiWrapper();
wrapper.getSmsCode(mobile)
        .subscribe(new Subscriber<String>() {
            @Override
            public void onCompleted() {
            }
            @Override
            public void onError(Throwable e) {
            }
            @Override
            public void onNext(String s) {
                Log.i(TAG, "call " + s);
            }
        });

通过以上代码,我们可以发现封装类有以下好处:

  • 传递某些固定参数,如上述代码中的String appType,或者userId等
  • 对网络请求返回结果进行分割操作
  • 可以进行线程控制

进一步封装

就只能这样了么?

public Observable<String> getSmsCode(String mobile) {
    return getService().getSmsCode(mobile, "GRAVIDA")
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .flatMap(new Func1<Response<String>, Observable<String>>() {
                @Override
                public Observable<String> call(Response<String> stringResponse) {
                    return flatResponse(stringResponse);
                }
            });
}

从这个方法中,我们可以清楚地看到数据是如何在一系列操作符之间进行转换的。但是以后每个网络请求都将进行这样的重复操作。
如何将一组操作符重用于多个数据流中呢?例如,因为希望在工作线程中处理数据,在主线程中处理结果,然后分割网络请求结果。所以我会频繁使用subscribeOn()observeOn()flatMap()。如果我能够通过重用的方式,将这种逻辑运用到我所有的数据流中,将是一件多么棒的事。
RxJava提供了一种解决方案:Transformer(有转换器意思),一般情况下可以通过使用操作符Observable.compose()来实现。
Transformer实际上就是一个Func1<Observable<T>, Observable<R>>,换言之就是:可以通过它将一种类型的Observable转换成另一种类型的Observable,和调用一系列的内联操作符是一模一样的。

/**
 * http://www.jianshu.com/p/e9e03194199e
 * <p/>
 *
 * @param <T>
 * @return
 */
protected <T> Observable.Transformer<Response<T>, T> applySchedulers() {
    return new Observable.Transformer<Response<T>, T>() {
        @Override
        public Observable<T> call(Observable<Response<T>> responseObservable) {
            return responseObservable.subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .flatMap(new Func1<Response<T>, Observable<T>>() {
                        @Override
                        public Observable<T> call(Response<T> tResponse) {
                            return flatResponse(tResponse);
                        }
                    })
                    ;
        }
    };
}

恩,没错,这一部分内容参考了注释内的链接,大家可以去看下这篇帖子。
通过上面的方法,我们将Observable<Response<T>>转化成了Observable<T>,并把处理了线程调度、分割返回结果等操作组合了起来,达到了复用的目的。
现在APIServicegetSmsCode()可简化为:

public Observable<String> getSmsCode(String mobile) {
    return getService().getSmsCode(mobile, "GRAVIDA")
            .compose(this.<String>applySchedulers());
}

由于要经常调用applySchedulers()方法,可以考虑创造一个实例化Transformer,节省不必要的实例化对象。代码如下:

final Observable.Transformer transformer = new Observable.Transformer() {
    @Override
    public Object call(Object observable) {
        return ((Observable) observable).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .flatMap(new Func1() {
                    @Override
                    public Object call(Object response) {
                        return flatResponse((Response<Object>)response);
                    }
                })
                ;
    }
};
protected <T> Observable.Transformer<Response<T>, T> applySchedulers() {
    return (Observable.Transformer<Response<T>, T>) transformer;
}

Note

.flatMap(new Func1() {
    @Override
    public Object call(Object response) {
        return flatResponse((Response<Object>)response);
    }
})

flatResponse()进行类型强转的话,应该没问题吧?笔者暂时不确定,但目前也没发现什么问题,,,

封装Subscriber

在Activity中我们调用getSmsCode()代码如下:

ApiWrapper wrapper = new ApiWrapper();
showLoadingDialog();
wrapper.getSmsCode(mobile)
        .subscribe(new Subscriber<String>() {
            @Override
            public void onCompleted() {
            }
            @Override
            public void onError(Throwable e) {
            }
            @Override
            public void onNext(String s) {
                Log.i(TAG, "call " + s);
            }
        });

其实,对于大部分请求来说,我们只需处理onNext() 方法,默认在onCompleted()方法中hideLoadingDialog();在onError()方法中Toast对应的错误信息。
所以我们可以进一步封装Subscriber,代码如下:

/**
 * 创建观察者
 *
 * @param onNext
 * @param <T>
 * @return
 */
protected <T> Subscriber newSubscriber(final Action1<? super T> onNext) {
    return new Subscriber<T>() {
        @Override
        public void onCompleted() {
            hideLoadingDialog();
        }
        @Override
        public void onError(Throwable e) {
            if (e instanceof RetrofitUtil.APIException) {
                RetrofitUtil.APIException exception = (RetrofitUtil.APIException) e;
                showToast(exception.message);
            } else if (e instanceof SocketTimeoutException) {
                showToast(e.getMessage());
            } else if (e instanceof ConnectException) {
                showToast(e.getMessage());
            }
            Log.e(TAG, String.valueOf(e.getMessage()));
            //e.printStackTrace();
            hideLoadingDialog();
        }
        @Override
        public void onNext(T t) {
            if (!mCompositeSubscription.isUnsubscribed()) {
                onNext.call(t);
            }
        }
    };
}

onError()方法中,可以根据Throwable e的类型进行对应处理,其中APIException是我们自定义的异常,SocketTimeoutExceptionConnectException则是OKHttp返回的异常。
onCompleted()onError()中,我们都需要hideLoadingDialog()

subscribe()之后, Observable会持有 Subscriber的引用,这个引用如果不能及时被释放,将有内存泄露的风险。所以最好保持一个原则:要在不再使用的时候尽快在合适的地方(例如 onPause()onStop()等方法中)调用 unsubscribe()来解除引用关系,以避免内存泄露的发生。

我们可以在BaseActivity中声明一个对象

/**
 * 使用CompositeSubscription来持有所有的Subscriptions
 */
protected CompositeSubscription mCompositeSubscription;

onCreate()方法中初始化:

mCompositeSubscription = new CompositeSubscription();

onDestroy()unsubscribe()接触引用关系:

@Override
protected void onDestroy() {
    super.onDestroy();
    //一旦调用了 CompositeSubscription.unsubscribe(),这个CompositeSubscription对象就不可用了,
    // 如果还想使用CompositeSubscription,就必须在创建一个新的对象了。
    mCompositeSubscription.unsubscribe();
}

在Activity中调用网络请求时:

Subscription subscription = wrapper.getSmsCode2("15813351726")
        .subscribe(newSubscriber(new Action1<String>() {
            @Override
            public void call(String s) {
                Log.i(TAG, "call " + s);
            }
        }));
mCompositeSubscription.add(subscription);

所以在newSubscriber()中的onNext()方法中,我们需要事先判断mCompositeSubscription是否已经解除了引用。

---20160229更新---
代码地址在RxAndroidDemo
请看下篇RxJava+Retrofit框架Demo(二)

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

推荐阅读更多精彩内容