優雅的構建Android項目之RxAndroid+Retrofit網絡請求

注意

Retrofit 2.0+和Retrofit 2.0之前的版本語法上有差別,本文基于Retrofit2.1.0

什么是Retrofit?

retrofit是一款針對Android網絡請求的開源框架,它與okhttp一樣出自Square公司。Rotrofit2.0的網絡框架全部交給了okhttp來實現,Android N之后Apache的httpclient已經被Google從SDK中移除,Okhttp則成功上位。Retrofit的網絡請求實現風格與URLconnection和httpClient有較大的差別。創建請求的時候需要先創建基于注解的服務接口(不了解的可以先了解一下注解),進行網絡請求的時候再通過retrofit.creat()方法創建請求。

Retrofit中http POST/GET請求

Retrofit中的網絡請求都是通過注解方式的接口方法來表示的,此處只對常用的post和get請求進行說明,Retrofit還提供有put,delete等請求方式可自己研究文檔使用

post請求

  • Body對象作為post參數
@POST("user/login")
Call<User> login(@Body LoginInfo loginInfo);
  • Field方式
@FormUrlEncoded
@POST("user/login")
Call<User> login(@Field("username") String username,
                 @Field(password) String password);
  • FieldMap方式
@FormUrlEncoded
@POST("user/login")
Call<User> login(@FieldMap Map<String,String> map);

參數較多時建議用Body方式和FieldMap方式

get請求

  • 直接請求某一地址獲取列表
//接口我瞎寫的
@GET("news/toplist")
Call<ArrayList<News> news> getNewsList(); 
  • url拼接固定查詢條件
@GET("news/toplist?date=20161030")
Call<ArrayList<News> news> getNewsList(); 
  • url中拼接地址信息
@GEt("news/{city}/newslist")
Call<ArrayList<News> news> getCityNewsList(@Path("city") String city);
  • 通過Query注解添加其他查詢條件
@GET("news/{city}/newslist")
Call<ArrayList<News> news> getCityNewsList(@Path("city") String city
                                           @Query("date") String date
                                           @Query("newsType") String newsType);
  • 查詢條件較多時同樣有QueryMap注解方法供使用
@GET("news/{city}/newslist")
Call<ArrayList<News> news> getCityNewsList(@Path("city") String city
                                           @QueryMap<String, String> options);

通過上面的API方法會發現都是在進行請求條件的配置,假如我要給請求加請求頭怎么辦?放心,retrofit也有相應的注解。除了注解之外還有一個萬用的處理方法。

Header請求頭設置

  • 為請求添加固定請求頭
//添加單個固定請求頭
@Header("Cache-Control: max-aget-640000")
@GET("news/toplist?date=20161030")
Call<ArrayList<News> news> getNewsList(); 
//多個請求頭以數組的形式提交
@Header(
    {"Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
    })
@GET("news/toplist?date=20161030")
Call<ArrayList<News> news> getNewsList(); 
  • 動態添加請求頭
//添加動態請求頭,比如獲取的認證信息等
@GET("news/toplist?date=20161030")
Call<ArrayList<News> news> getNewsList(@Header(Authorization) String authorization); 

上面的兩種添加請求頭的方法作用范圍只是添加注解的單個方法,如果想為每個請求都添加請求頭還按這種方式來做的話就很不程序猿了。Retrofit的網絡請求全部交給okhttp來處理,因此我們可以通過OkHttpClient來做文章,自己重寫okhttp的攔截器在攔截器內再進行需要的操作

Okhttp interceptor

攔截器顧名思義,所有通過okhttp進行的請求都會過一遍okhttpClient的攔截器,發出去的請求,收到的響應都會經過他,就像一個雙向的安檢通道。
okhttp攔截器的原理如下:

okhttp攔截器,圖片來自okhttp官網
okhttp攔截器,圖片來自okhttp官網

如圖所示攔截器分為Application Interceptors和NetWork Interceptors。Application攔截器工作區域為應用發出請求到okhttp核心之間,遠端響應經過okhttp核心后到達應用處理之前。而NetWork攔截器的作用域為okhtt核心到遠端服務器之間的部分。明顯區別就是當一次請求中會有一個重定向的時候Application攔截器只會響應一次,因為對于應用來說就進行了一次請求。而NetWork攔截器會在重定向時也響應即響應兩次,也不難理解,畢竟重定向也會經過一次okhttp核心嘛。

攔截器工作示意簡圖
攔截器工作示意簡圖

上圖是okhttp攔截器工作原理簡圖,重點在右邊部分。當多個攔截器配合使用時,不用擔心請求攔截和響應攔截順序會錯亂,okhttp已經給你排好了。


上傳個需要壓縮和編碼的東東的時候,你可以選擇先寫個攔截器請求時壓縮響應時解壓,再寫個攔截器請求時編碼響應時解碼。加起來就是壓縮->編碼->okhttClient與服務器的Py交易->解碼->解壓跟棧先進后出類似。

原理扯了一大堆,代碼才是干貨,看了代碼才知道怎么用。

//官方的栗子
class LoggingInterceptor implements Interceptor {
        @Override
        public Response intercept(Interceptor.Chain chain) throws IOException {
         //拿到request實例在此對請求做需要的設置
            Request request = chain.request();
            long t1 = System.nanoTime();
            logger.info(String.format("Sending request %s on %s%n%s",
                    request.url(), chain.connection(), request.headers()));
            //發送request請求
            Response response = chain.proceed(request);
            //得到請求后的response實例,做相應操作
            long t2 = System.nanoTime();
            logger.info(String.format("Received response for %s in %.1fms%n%s",
                    response.request().url(), (t2 - t1) / 1e6d, response.headers()));
            return response;
        }
    }

通過Request request = chain.request();拿到請求實例,想怎么裝扮就怎么裝扮,什么加請求頭,設置編碼格式soeasy。前面說到的為每個請求設置請求頭就是在這完成設置工作的。但是真正要加到請求里跟retrofit的ApiService接口一起用還需要將Okhttp注冊攔截器后與Retrofit綁定才行。

//注冊應用攔截器
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();
Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();··
Response response = client.newCall(request).execute();
response.body().close();

//注冊網絡攔截器
OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();
Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();
Response response = client.newCall(request).execute();
response.body().close();

該如何使用Retrofit?

基本也說得差不多了,那么怎么使用Retrofit進行一次完整的網絡請求呢
需要注意一下Retrofit的Url拼接規則

enter description here
enter description here

enter description here
enter description here

enter description here
enter description here

個人建議以第一幅圖的方式,baseUrl總是以/結尾,接口rul總是不以/開頭

  • 1、當然是引入retrofit的庫啦
//build.gradle的依賴中加入,其中第二條不一定要使用gson,其他方式在官方的github上也有
// retrofit
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
  • 2、創建一個ServiceApi接口
//方便后面RxAndroid我把RxAndroid方式的接口也貼上來。只有返回類型不同而已
public interface RetrofitService {
    //單純使用retrofit接口定義
    @GET("news/latest")
    Call<ZhiHuDaily> getZhihuDailyRetrofitOnly();

    //使用retrofit+RxAndroid的接口定義
    @GET("news/latest")
    Observable<ZhiHuDaily> getZhihuDaily();
}
  • 3、我建議是維護一個統一的api管理類。當然你要直接拿接口用也行,但可維護性會降低很多
public class ApiManager {

    private RetrofitService mDailyApi;
    private static ApiManager sApiManager;
    //獲取ApiManager的單例
    public static ApiManager getInstence() {
        if (sApiManager == null) {
            synchronized (ApiManager.class) {
                if (sApiManager == null) {
                    sApiManager = new ApiManager();
                }
            }
        }
        return sApiManager;
    }
    /**
     * 封裝配置知乎API
     */
    public RetrofitService getDailyService() {
    //不需要使用攔截器就不創建直接從if開始
        OkHttpClient client = new OkHttpClient.Builder()
                //添加應用攔截器
                .addInterceptor(new MyOkhttpInterceptor())
                //添加網絡攔截器
//                .addNetworkInterceptor(new MyOkhttpInterceptor())
                .build();
        if (mDailyApi == null) {
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(GlobalConfig.baseUrl)
                    //將client與retrofit關聯
                    .client(client)
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
            //到這一步創建完成
            mDailyApi = retrofit.create(RetrofitService.class);
        }
        return mDailyApi;
    }
}
  • 4、調用接口方法進行網絡請求
    public void getStoryDataByRetrofit(final OnEventLister<ArrayList<ZhihuStory>> eventLister) {
        ApiManager apiManager = ApiManager.getInstence();
        Call<ZhiHuDaily> call = apiManager.getDailyService().getZhihuDailyRetrofitOnly();
        //發送異步請求
        call.enqueue(new Callback<ZhiHuDaily>() {
            @Override
            public void onResponse(Call<ZhiHuDaily> call, Response<ZhiHuDaily> response) {
                eventLister.onSuccess(response.body().getStories());
            }

            @Override
            public void onFailure(Call<ZhiHuDaily> call, Throwable t) {
                eventLister.onFail(t.getMessage(), "");
            }
        });
    }

使用Retrofit的好處

  • 可以少寫不少的代碼
  • 接口方便維護需要改什么直接到ApiService中進行配置即可
  • 異步請求不再需要自己來newThread再handler,也不需要自己再來寫請求結果回調。異步請求只需要使用call.enqueue()即可。
  • 支持RxAndroid,這個我覺得很重要
  • 降低工程的耦合度,網絡請求跟邏輯代碼完全剝離開。需要的僅僅是傳遞參數有的請求甚至參數都不需要傳遞。直接在接口中配置就好。

Retrofit的好基友——RxAndroid

RxAndroid是RxJava在Android上的變種。那么RxJava到底是什么呢?
"a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個在 Java VM 上使用可觀測的序列來組成異步的、基于事件的程序的庫)。這是github項目主頁的自我概括,我覺得其實就兩個關鍵詞,異步基于事件。這里我只說一下RxAndroid怎么跟Retrofit搭配使用,要進一步了解可以異步扔物線大神的文章給 Android 開發者的 RxJava 詳解。講的肯定比我好,我就是看這個入門的。

Retrofit+RxAndroid使用

因為用到Retrofit所以定義接口,創建ApiManager這些跟上面單純用Retrofit是一毛一樣的,唯一的不同是接口的返回類型從Retrofit的Call對象變成了Observable對象,即被觀察者對象。然后就是調用進行網絡請求部分變成如下形式

    //使用rxandroid+retrofit進行請求
    public void loadDataByRxandroidRetrofit() {
        mIMainActivity.showProgressBar();
        Subscription subscription = ApiManager.getInstence().getDailyService()
                .getZhihuDaily()
                .map(new Func1<ZhiHuDaily, ArrayList<ZhihuStory>>() {
                    @Override
                    public ArrayList<ZhihuStory> call(ZhiHuDaily zhiHuDaily) {
                        return zhiHuDaily.getStories();
                    }
                })
                //設置事件觸發在非主線程
                .subscribeOn(Schedulers.io())
                //設置事件接受在UI線程以達到UI顯示的目的
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<ArrayList<ZhihuStory>>() {
                    @Override
                    public void onCompleted() {
                        mIMainActivity.hidProgressBar();
                    }

                    @Override
                    public void onError(Throwable e) {
                        mIMainActivity.getDataFail("", e.getMessage());
                    }

                    @Override
                    public void onNext(ArrayList<ZhihuStory> stories) {
                        mIMainActivity.getDataSuccess(stories);
                    }
                });
        //綁定觀察對象,注意在界面的ondestory或者onpouse方法中調用presenter.unsubcription();進行解綁避免內存泄漏
        addSubscription(subscription);
    }

網絡請求得到的是一個Obserable對象,該對象再通過subscrible()綁定一個觀察者對象,觀察者對象中有onCompleted(),onError(),onNext()三個回調方法。事件過程中出錯onError()觸發并停止后續事件,一個Obserable對象一次發出多個事件每次都會觸發onNext(),當不再有事件發出的時候onCompleted()方法觸發并結束。異步在RxAndroid中變得很簡單subscribeOn指定事件發生線程,如上面的網絡請求被指定在io線程中,observeOn指定事件的消費線程,如上面的知乎故事數據結果被交給主線程顯示。

RxAndroid的優點

看完上面單獨使用retrofit和使用retrofit+rxandroid兩種方式之后你也許會吐槽,尼瑪腦子有坑?明明代碼變多了。
但是你也會發現代碼中都是.XX()的形式如果需求變化更多一些會更明顯。比如說就上面的例子我要在每一條信息中修改某一個值。并且又要對結果進行一些篩選。只用retrofit的話是不是應該向這樣:

    call.enqueue(new Callback<ZhiHuDaily>() {
        @Override
        public void onResponse(Call<ZhiHuDaily> call, Response<ZhiHuDaily> response) {
            ArrayList<ZhihuStory> stories = response.body().getStories();
            for(ZhihuStory story : stories){
                //修改每一條story中的某一值,這里用XXX代替
                story.setXXX(XXX);
                //篩選出id<100的
                if(story.getId()>100){
                    stories.remove(story);
                }
            }
            eventLister.onSuccess(response.body().getStories());
        }

        @Override
        public void onFailure(Call<ZhiHuDaily> call, Throwable t) {
            eventLister.onFail(t.getMessage(), "");
        }
    });

如果需要設置和條件賽選層次越多會發現for套if,if再if會越嵌套越多。隔一段時間之后就真的成了“當初寫下這段代碼的時候只有我跟上帝知道他是干嘛的,現在只有上帝知道他是干嘛的”。而使用RxAndroid的話整個變換過程都是線性的哪一步做了什么都會很清楚不會出現各種蜜汁縮進:

        Subscription subscription = ApiManager.getInstence().getDailyService()
                .getZhihuDaily()
                //從ZhihuDaily中獲取Stories列表
                .map(new Func1<ZhiHuDaily, ArrayList<ZhihuStory>>() {
                    @Override
                    public ArrayList<ZhihuStory> call(ZhiHuDaily zhiHuDaily) {
                        return zhiHuDaily.getStories();
                    }
                })
                //將列表拆開成事件發送
                .flatMap(new Func1<ArrayList<ZhihuStory>, Observable<ZhihuStory>>() {
                    @Override
                    public Observable<ZhihuStory> call(ArrayList<ZhihuStory> stories) {
                        return Observable.from(stories);
                    }
                })
                //將story中的XXX設置為xxx
                .map(new Func1<ZhihuStory, ZhihuStory>() {
                    @Override
                    public ZhihuStory call(ZhihuStory zhihuStory) {
                        zhihuStory.setXXX(xxx);
                        return zhihuStory;
                    }
                })
                //過濾掉Id>10的story
                .filter(new Func1<ZhihuStory, Boolean>() {
                    @Override
                    public Boolean call(ZhihuStory zhihuStory) {
                        return zhihuStory.getId()<10;
                    }
                })
                //將結果重新整理成List
                .toList()
                //設置事件觸發在非主線程
                .subscribeOn(Schedulers.io())
                //設置事件接受在UI線程以達到UI顯示的目的
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<List<ZhihuStory>>() {
                    @Override
                    public void onCompleted() {
                        mIMainActivity.hidProgressBar();
                    }

                    @Override
                    public void onError(Throwable e) {
                        mIMainActivity.getDataFail("", e.getMessage());
                    }

                    @Override
                    public void onNext(List<ZhihuStory> stories) {
                        mIMainActivity.getDataSuccess((ArrayList<ZhihuStory>) stories);
                    }
                });

越復雜的邏輯,Rx的優勢也就越明顯。Rx的操作符各種組合起來幾乎能夠滿足所有的變換需求。開始寫可能會覺得很不適應,但熟練使用之后會默念Rx大法好的。
對于RxJava操作符鼠標懸停都會有文字和示意圖的,另外發現一個不錯的博客里面也有較詳細的解析RxJava/RxAndroid操作符

Demo地址

在之前的那個MVPDemo的基礎上寫的,okhttp請求方式,單純retrofit方式,retrofit+rxAndroid方式的請求都有保留,可以對比著感受一下。
demo倉庫地址

覺得本文對你有幫助

關注簡書PandaQ404,持續分享中。。。
Github主頁

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容