1. 程式人生 > >構建Android專案之RxAndroid+Retrofit網路請求

構建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操作符