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.lxweimin.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閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容