Retrofit 2詳解和使用(4)——彙總

Retrofit簡介

Retrofit是大名鼎鼎的 Square 公司開源的適用於AndroidJava的網絡請求庫,官方的介紹非常簡短

A type-safe HTTP client for Android and Java

Retrofit使用註解,能夠極大的簡化網絡請求,在2.0版本默認使用Square自家的OkHttp作爲底層Http Client,關於如何使用OkHttp配合Retrofit本文後面也會講到。首先在build.gradle中加入

    implementation 'com.squareup.retrofit2:retrofit:2.3.0'

定義一個網絡請求:

public interface ZhiHuApi {
@GET("users")
Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit將網絡請求轉變成了Java interface的形式,interface要獲得實例調用listRepos(String user),需要藉助Retrofit.java這個類,通過Retrofit.Builder來配置Retrofit,再通過retrofit.create(final Class<T> service)獲取接口的實例

class Factory {
  public static ZhiHuApi create() {
    Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("http://news-at.zhihu.com/")
                    .build();
            return retrofit.create(ZhiHuApi.class);
        }
}

@GET註解表示請求方式爲GET,其中的字符串表示相對URL,而scheme host port被認爲是BaseUrl設置到Retrofit中,注意BaseUrl需要以/結尾,這樣每個接口定義的相對URL就不需要以/開始。如果在接口中定義的URL爲全路徑,將用這個全路徑作爲請求URLBaseUrl將不起作用。@Path標識get請求的請求參數,上面的listRepos可以認爲是在請求http://news-at.zhihu.com/users?user=user
@POST註解表示請求方式爲POST,通常和@FormUrlEncoded註解一起使用,在使用@FormUrlEncoded時可以使用@Field標識表單字段

@FormUrlEncoded
@POST("user/login.do")
Call<User> login(@Field("username") String userName, @Field("password") String password);

或者使用@FieldMap提交整個map

@FormUrlEncoded
@POST("user/login.do")
Call<User> login(@FieldMap Map<String, String> formMap);

當然你也可以把整個表單封裝爲一個實體,使用@Body一次提交

@FormUrlEncoded
@POST("user/login.do")
Call<User> login(@Body User user);

Multipart請求時使用@Multipart註解,用@Part標識每個RequestBody

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

定義好請求之後就可以調用call.enqueue()來執行請求,需要傳入Callback<T>,其中T的類型編譯器會根據Call<T>中的類型來判斷,Retrofit和其他網絡請求庫一樣對於Android 平臺做了線程切換,請求在後臺執行,Callback<T>會回到main (UI) thread,如果是Java程序Callback<T>會繼續回到調用它的線程。

ZhiHuApi zhiHuApi = BaseNetwork.Factory.create(ZhiHuApi.class);
        Call<User> call = zhiHuApi.login("username", "pwd");
        call.enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
            }
            @Override
            public void onFailure(Call<User> call, Throwable t) {
            }
        });

爲請求添加請求頭時使用@Headers,這裏就不做舉例,因爲app中通常是每個請求都需要攜帶請求頭,不建議在Retrofit定義請求時傳入,而是使用OkHttp來實現統一請求頭。

Converter

Retrofit在默認情況下只能將Http的響應體反序列化到OkHttpResponseBody中,加入Converter可以將返回的數據直接格式化成你需要的樣子,現有6個Converter可以直接使用:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

之後在代碼里加入(此處以GsonConverterFactory爲例)

 Retrofit retrofit =new Retrofit.Builder()
                    .baseUrl(Constants.BASE_HTTP_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

返回的數據會使用Gson解析爲對應傳入的實體類,你也可以自定義Converter來實現更復雜的需求,只需要extends Converter.Factory然後重寫

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
      Retrofit retrofit) {
        //your own implements
  }
  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
     Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
       //your own implements
  }

Retrofit終歸只是應用層的api,真正的執行器是OkHttp,較爲複雜的需求都需要從執行層入手,可以做到Retrofit對外不變的多種自定義統一封裝。

OkHttp配合Retrofit使用

前文已經提到在Retrofit 2.0中已經默認使用OkHttp作爲網絡請求執行器,關於OkHttp的優點簡單提一下:(原文鏈接)

  • 1.支持HTTP2/SPDY黑科技
  • 2.socket自動選擇最好路線,並支持自動重連
  • 3.擁有自動維護的socket連接池,減少握手次數
  • 4.擁有隊列線程池,輕鬆寫併發
  • 5.擁有Interceptors輕鬆處理請求與響應(比如透明GZIP壓縮,LOGGING)
  • 6.基於Headers的緩存策略

想要使用OkHttpRetrofit提供更高的定製性,給Retrofit設置自定義的OkHttpClient就可以了

Retrofit retrofit = new Retrofit.Builder()
                  .baseUrl(Constants.BASE_HTTP_URL)
                  .client(client)
                  .build();

之後就是構建一個OkHttpClient

OkHttpClient client = new OkHttpClient.Builder()
           // 向Request Header添加一些業務相關數據,如APP版本,token
           .addInterceptor(new HeadInterceptor())
           //日誌Interceptor,可以打印日誌
           .addInterceptor(logging)
           // 連接超時時間設置
           .connectTimeout(10, TimeUnit.SECONDS)
           // 讀取超時時間設置
           .readTimeout(10, TimeUnit.SECONDS)
           // 失敗重試
           .retryOnConnectionFailure(true)
           // 支持Https需要加入SSLSocketFactory
           .sslSocketFactory(sslSocketFactory)
           // 信任的主機名,返回true表示信任,可以根據主機名和SSLSession判斷主機是否信任
           .hostnameVerifier(new HostnameVerifier() {
               @Override
               public boolean verify(String hostname, SSLSession session) {
                   return true;
               }
           })
           // 使用host name作爲cookie保存的key
           .cookieJar(new CookieJar() {
               private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();

               @Override
               public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                   cookieStore.put(HttpUrl.parse(url.host()), cookies);
               }

               @Override
               public List<Cookie> loadForRequest(HttpUrl url) {
                   List<Cookie> cookies = cookieStore.get(HttpUrl.parse(url.host()));
                   return cookies != null ? cookies : new ArrayList<Cookie>();
               }
           })
           .build();

如果設置了sslSocketFactory卻沒有配置對應的hostnameVerifier,那麼Https請求是無法成功的。上面用到兩個Interceptor分別是HeadInterceptorHttpLoggingInterceptor,分別是用來添加請求頭和打印請求日誌的攔截器,OkHttp支持自定義攔截器,例如下面代碼自定義的HeadInterceptor爲請求加入Headers

public class HeadInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        Request compressedRequest = originalRequest.newBuilder()
                .headers(Headers.of(getHeaders()))
                .build();
        return chain.proceed(compressedRequest);
    }
}

有時服務器會對POST提交的表單做參數校驗,一種方式是在請求頭裏加入特定方式加密過的表單參數的Map,那麼就需要先獲取到請求的Map,通過FormBody可以實現

// if the server needs to verify post params, use this to get post params;
  RequestBody oidBody = originalRequest.body();
  Map<String, String> params = new HashMap<>();
  if (oidBody instanceof FormBody) {
    FormBody formBody = (FormBody) oidBody;
    for (int i = 0; i < formBody.size(); i++) {
        params.put(formBody.encodedName(i), formBody.encodedValue(i));
    }
  }

HttpLoggingInterceptorSquare 提供的請求信息日誌打印工具類,如果需要可以在build.gradle中加入

 compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'

可以根據不同情況配置日誌輸出的Level

  • NONE 不輸出日誌
  • BASIC 只輸出請求方式響應碼等基本信息
  • HEADERS 只輸出請求和響應的頭部信息
  • BODY 輸出請求和響應的頭部和請求體信息

另外如果遇到兩個接口有相互依賴關係,必須請求完第一個接口拿到數據後才知道第二個請求的URL,通常我們會定義兩個Retrofit,因爲RetrofitBaseUrl是統一配置的,不過現在可以通過實現動態BaseUrl來避免這個問題,先看DynamicBaseUrlInterceptor的代碼

public class DynamicBaseUrlInterceptor implements Interceptor {
    private volatile String host;

    public void setHost(String host) {
        this.host = host;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        if (!TextUtils.isEmpty(host)) {
            HttpUrl newUrl = originalRequest.url().newBuilder()
                    .host(host)
                    .build();
            originalRequest = originalRequest.newBuilder()
                    .url(newUrl)
                    .build();
        }

        return chain.proceed(originalRequest);
    }
}

BaseUrl改變時只需要setHost()就可以讓下次請求的Baseurl改變

Retrofit 與 RxJava 結合使用

本節需要對RxJava基本用法有了解,如果不瞭解可以忽略或者先去熟悉一下RxJavawiki,介紹的目的是因爲兩者結合使用確實很方便,關於RxJava之後會單獨寫。

RxJavaRx(全稱Reactive Extensions)家族中的一員,是最近很火的響應式編程庫,官方對於它的解釋很簡單

RxJava – Reactive Extensions for the JVM – a library for composing asynchronous 
and event-based programs using observable sequences for the Java VM.

一個異步的基於事件的觀察者序列,可以理解爲擴展的觀察者模式,在 Android 中使用RxJava需要引入兩個compileRxAndroid是專爲 Android 平臺打造來提供主線程切換等便利的工具項目。

   compile 'io.reactivex:rxandroid:1.2.0'
   // Because RxAndroid releases are few and far between, it is recommended you also
   // explicitly depend on RxJava's latest version for bug fixes and new features.
   compile 'io.reactivex:rxjava:1.1.5'

Retrofit提供了CallAdapterFactory,它是一個知道如何將call實例轉換成其他類型的工廠類,目前支持的有:

  • RxJava
  • Guava
  • Java8

這些和Retrofit本身都是分離的,需要單獨引入compile例如

compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'

在代碼中配置CallAdapterFactory

Retrofit retrofit =new Retrofit.Builder()
                   .baseUrl(Constants.BASE_HTTP_URL)
                   .addConverterFactory(GsonConverterFactory.create())
                   .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                   .build();

之後就可以把請求的返回改爲Observable<T>

@GET("users")
Observable<List<Repo>> listRepos(String user);

請求時只需要

BaseNetwork.Factory.create(Foo.class)
                .listRepos("user")
                .observeOn(AndroidSchedulers.mainThread())//觀察者所在的線程
                .subscribeOn(Schedulers.io())//請求執行的線程
                //如果正常執行會順序調用onNext,onCompleted,如果出錯則會調用onError
                .subscribe(new Observer<List<Repo>>() {
                    @Override
                    public void onCompleted() {
                    }
                    @Override
                    public void onError(Throwable e) {
                    }
                    @Override
                    public void onNext(List<Repo> list) {
                    }
                });

如果需要配合服務器返回固定的格式,通過狀態碼判斷業務是否出錯,如果出錯獲取錯誤信息,類似如下格式

{
  "state":0,//狀態碼,0爲業務正常
  "msg":"",//如果業務出錯,攜帶錯誤信息
  "data":{}//包含實際業務實體
}

需要定義統一的響應實體,根據T傳入的類型來獲取業務實體真實的類型

public class BaseResult<T> {
    private int state;
    private String msg;
    private T data;
    public int getState() {
        return state;
    }
    public void setState(int state) {
        this.state = state;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

請求中的泛型類型需要是BaseResult<T>

@GET("users")
Observable<BaseResult<List<Repo>>> listRepos(@Path("user") String user);

調用時也會有改變,需要經過一次拆解統一返回,處理錯誤的過程

BaseNetwork.Factory.create(Foo.class)
                .listRepos("user")
                .flatMap(new NetworkResultFunc1<List<Repo>>())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(observer);

flatMap需要傳入Func1<T, R>,Func1<T, R>繼承了Function,只有一個方法,將泛型參數列表的第一個轉換爲第二個返回,它可以將Observable做一個展開,並返回一個新的Observable

public interface Func1<T, R> extends Function {
    R call(T t);
}

NetworkResultFunc1<List<Repo>>實現了Func1<T, R>,代碼如下

public class NetworkResultFunc1<T> implements Func1<BaseResult<T>, Observable<T>> {

    @Override
    public Observable<T> call(final BaseResult<T> tBaseResult) {
        return Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(Subscriber<? super T> subscriber) {
                int state = tBaseResult.getState();
                String msg = tBaseResult.getMsg();
                switch (state) {
                    case 0://if success, return data to client
                        subscriber.onNext(tBaseResult.getData());
                        break;
                    case 1000://if this means error
                        subscriber.onError(new ApiException(state, msg));
                        break;
                }
                subscriber.onCompleted();//no error, will execute onCompleted()
            }
        });
    }
}

如果state爲0,則調用subscriber.onNext()向調用者返回數據,當state不等於0時意味着業務出錯了,向subscriber.onError()中拋了一個ApiException,這樣在Observer處會回調onError()終止整個事件流,調用者也能獲得業務錯誤的相關信息。ApiException代碼如下,就是一個自定義的RuntimeException

public class ApiException extends RuntimeException {
    private int state;
    private String msg;
    public ApiException(int state, String msg) {
        this.state = state;
        this.msg = msg;
    }

    public int getState() {
        return state;
    }

    public String getMsg() {
        return msg;
    }
}

對於Retrofit的介紹就先到這裏,相信看到這裏,你已經能夠在項目中優雅的使用Retrofit了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章