1. 程式人生 > >Retrofit2.0使用總結及注意事項

Retrofit2.0使用總結及注意事項

概述

隨著Google對HttpClient 摒棄,和Volley的逐漸沒落,OkHttp開始異軍突起,而Retrofit則對okHttp進行了強制依賴。

Retrofit是由Square公司出品的針對於Android和Java的型別安全的Http客戶端,

如果看原始碼會發現其實質上就是對okHttp的封裝,使用面向介面的方式進行網路請求,利用動態生成的代理類封裝了網路介面請求的底層,

其將請求返回javaBean,對網路認證 REST API進行了很好對支援此,使用Retrofit將會極大的提高我們應用的網路體驗。

REST

既然是RESTful架構,那麼我們就來看一下什麼是REST吧。
REST(REpresentational State Transfer)是一組架構約束條件和原則。
RESTful架構都滿足以下規則:
(1)每一個URI代表一種資源;
(2)客戶端和伺服器之間,傳遞這種資源的某種表現層;
(3)客戶端通過四個HTTP動詞,對伺服器端資源進行操作,實現”表現層狀態轉化”。

2.0與1.9使用比較

如果之前使用過Retrofit1,會發現2.0後的API會有一些變化,
比如建立方式,攔截器,錯誤處理,轉換器等,如下我們列舉以下他們使用起來具體的區別有哪些。

(1) 在Retrofit1中使用的是RestAdapter,而Retrofit2中使用的Retrofit例項,之前的setEndpoint變為了baseUrl。
(2) Retrofit1中使用setRequestInterceptor設定攔截器,對http請求進行相應等處理。
(3) Retrofit2通過OKHttp的攔截器攔截http請求進行監控,重寫或重試等,包括日誌列印等。
(4) converter,Retrofit1中的setConverter,換以addConverterFactory,用於支援Gson轉換。

Retrofit1體驗不好的地方:

(1) Retrofit1不能同時操作response返回資料(比如說返回的 Header 部分或者 URL)和序列化後的資料(JAVABEAN)
(2) Retrofit1中同步和非同步執行同一個方法需要分別定義介面。
(3) Retrofit1對正在進行的網路任務無法取消。

1.9使用配置:

        //gson converter
        final static Gson gson = new GsonBuilder()
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
) .serializeNulls() .create(); // client OkHttpClient client = new OkHttpClient(); client.setReadTimeout(12, TimeUnit.SECONDS); RestAdapter.Builder builder = new RestAdapter.Builder(); builder.setClient(new OkClient(client)) //日誌列印 .setLogLevel(RestAdapter.LogLevel.FULL) //baseUrl .setEndpoint("https://api.github.com") //轉換器 .setConverter(new GsonConverter(gson)) //錯誤處理 .setErrorHandler(new ErrorHandler() { @Override public Throwable handleError(RetrofitError cause) { return null; } }) //攔截器 .setRequestInterceptor(authorizationInterceptor) RestAdapter restAdapter = builder.build(); apiService = restAdapter.create(ApiService.class);

2.0使用配置

引入依賴

    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'

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

OkHttp配置

   HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        client = new OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .retryOnConnectionFailure(true)
                .connectTimeout(15, TimeUnit.SECONDS)
                .addNetworkInterceptor(authorizationInterceptor)
                .build();

其中 level 為 BASIC / HEADERS / BODY,BODY等同於1.9中的FULL
retryOnConnectionFailure:錯誤重聯
addInterceptor:設定應用攔截器,可用於設定公共引數,頭資訊,日誌攔截等
addNetworkInterceptor:網路攔截器,可以用於重試或重寫,對應與1.9中的setRequestInterceptor。
參考:Interceptors
中文翻譯:Okhttp-wiki 之 Interceptors 攔截器

Retrofit配置

        Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();
        apiService = retrofit.create(ApiService.class);

其中baseUrl相當於1.9中的setEndPoint,
addCallAdapterFactory提供RxJava支援,如果沒有提供響應的支援(RxJava,Call),則會跑出異常。
addConverterFactory提供Gson支援,可以新增多種序列化Factory,但是GsonConverterFactory必須放在最後,否則會丟擲異常。
參考:用 Retrofit 2 簡化 HTTP 請求

2.0使用介紹

注意:retrofit2.0後:BaseUrl要以/結尾;@GET 等請求不要以/開頭;@Url: 可以定義完整url,不要以 / 開頭。
關於URL拼接注意事項:Retrofit 2.0:有史以來最大的改進

基本用法:

//定以介面
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

//獲取例項
Retrofit retrofit = new Retrofit.Builder()
    //設定OKHttpClient,如果不設定會提供一個預設的
    .client(new OkHttpClient())
    //設定baseUrl
    .baseUrl("https://api.github.com/")
    //新增Gson轉換器
    .addConverterFactory(GsonConverterFactory.create())
    .build();

GitHubService service = retrofit.create(GitHubService.class);

//同步請求
//https://api.github.com/users/octocat/repos
Call<List<Repo>> call = service.listRepos("octocat");
try {
     Response<List<Repo>> repos  = call.execute();
} catch (IOException e) {
     e.printStackTrace();
}

//call只能呼叫一次。否則會拋 IllegalStateException
Call<List<Repo>> clone = call.clone();

//非同步請求
clone.enqueue(new Callback<List<Repo>>() {
        @Override
        public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
            // Get result bean from response.body()
            List<Repo> repos = response.body();
            // Get header item from response
            String links = response.headers().get("Link");
            /**
              * 不同於retrofit1 可以同時操作序列化資料javabean和header
              */
        }

        @Override
        public void onFailure(Call<List<Repo>> call, Throwable t) {

        }
    });

// 取消
call.cancel();
//rxjava support
public interface GitHubService {
  @GET("users/{user}/repos")
  Observable<List<Repo>> listRepos(@Path("user") String user);
}

// 獲取例項
// Http request
Observable<List<Repo>> call = service.listRepos("octocat");

retrofit註解:

方法註解,包含@GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS、@HTTP。
標記註解,包含@FormUrlEncoded、@Multipart、@Streaming。
引數註解,包含@Query,@QueryMap、@Body、@Field,@FieldMap、@Part,@PartMap。

其他註解,@Path、@Header,@Headers、@Url

幾個特殊的註解
@HTTP:可以替代其他方法的任意一種

   /**
     * method 表示請的方法,不區分大小寫
     * path表示路徑
     * hasBody表示是否有請求體
     */
    @HTTP(method = "get", path = "users/{user}", hasBody = false)
    Call<ResponseBody> getFirstBlog(@Path("user") String user);

@Url:使用全路徑複寫baseUrl,適用於非統一baseUrl的場景。

@GET
Call<ResponseBody> v3(@Url String url);

@Streaming:用於下載大檔案

@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlAsync(@Url String fileUrl);  
ResponseBody body = response.body();
long fileSize = body.contentLength();
InputStream inputStream = body.byteStream();

常用註解
@Path:URL佔位符,用於替換和動態更新,相應的引數必須使用相同的字串被@Path進行註釋

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);
//--> http://baseurl/group/groupId/users

//等同於:
@GET
Call<List<User>> groupListUrl(
      @Url String url);

@Query,@QueryMap:查詢引數,用於GET查詢,需要注意的是@QueryMap可以約定是否需要encode

@GET("group/users")
Call<List<User>> groupList(@Query("id") int groupId);
//--> http://baseurl/group/users?id=groupId
Call<List<News>> getNews((@QueryMap(encoded=true) Map<String, String> options);  

@Body:用於POST請求體,將例項物件根據轉換方式轉換為對應的json字串引數,
這個轉化方式是GsonConverterFactory定義的。

 @POST("add")
 Call<List<User>> addUser(@Body User user);

@Field,@FieldMap:Post方式傳遞簡單的鍵值對,
需要新增@FormUrlEncoded表示表單提交
Content-Type:application/x-www-form-urlencoded

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

@Part,@PartMap:用於POST檔案上傳
其中@Part MultipartBody.Part代表檔案,@Part(“key”) RequestBody代表引數
需要新增@Multipart表示支援檔案上傳的表單,Content-Type: multipart/form-data

 @Multipart
    @POST("upload")
    Call<ResponseBody> upload(@Part("description") RequestBody description,
                              @Part MultipartBody.Part file);
    // https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
    // use the FileUtils to get the actual file by uri
    File file = FileUtils.getFile(this, fileUri);

    // create RequestBody instance from file
    RequestBody requestFile =
            RequestBody.create(MediaType.parse("multipart/form-data"), file);

    // MultipartBody.Part is used to send also the actual file name
    MultipartBody.Part body =
            MultipartBody.Part.createFormData("picture", file.getName(), requestFile);

    // add another part within the multipart request
    String descriptionString = "hello, this is description speaking";
    RequestBody description =
            RequestBody.create(
                    MediaType.parse("multipart/form-data"), descriptionString);

@Header:header處理,不能被互相覆蓋,用於修飾引數,

//動態設定Header值
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

等同於 :

//靜態設定Header值
@Headers("Authorization: authorization")//這裡authorization就是上面方法裡傳進來變數的值
@GET("widget/list")
Call<User> getUser()

@Headers 用於修飾方法,用於設定多個Header值:

@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

自定義Converter

retrofit預設情況下支援的converts有Gson,Jackson,Moshi…

要自定義Converter<F, T>,需要先看一下GsonConverterFactory的實現,
GsonConverterFactory實現了內部類Converter.Factory。

其中GsonConverterFactory中的主要兩個方法,主要用於解析request和response的,
在Factory中還有一個方法stringConverter,用於String的轉換。

//主要用於響應體的處理,Factory中預設實現為返回null,表示不處理
 @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
      Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonResponseBodyConverter<>(gson, adapter);
  }

/**
  *主要用於請求體的處理,Factory中預設實現為返回null,不能處理返回null
  *作用物件Part、PartMap、Body
  */
  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
      Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonRequestBodyConverter<>(gson, adapter);
  }
//Converter.Factory$stringConverter
/**
  *作用物件Field、FieldMap、Header、Path、Query、QueryMap
  *預設處理是toString
  */
  public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
          Retrofit retrofit) {
        return null;
      }

GsonRequestBodyConverter實現了Converter<F, T>介面,
主要實現了轉化的方法

T convert(F value) throws IOException;

自定義Interceptor

Retrofit 2.0 底層依賴於okHttp,所以需要使用okHttp的Interceptors 來對所有請求進行攔截。
我們可以通過自定義Interceptor來實現很多操作,列印日誌,快取,重試等等。

要實現自己的攔截器需要有以下步驟

(1) 需要實現Interceptor介面,並複寫intercept(Chain chain)方法,返回response
(2) Request 和 Response的Builder中有header,addHeader,headers方法,需要注意的是使用header有重複的將會被覆蓋,而addHeader則不會。

標準的 Interceptor寫法

public class OAuthInterceptor implements Interceptor {

  private final String username;
  private final String password;

  public OAuthInterceptor(String username, String password) {
    this.username = username;
    this.password = password;
  }

  @Override public Response intercept(Chain chain) throws IOException {

    String credentials = username + ":" + password;

    String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);

    Request originalRequest = chain.request();
    String cacheControl = originalRequest.cacheControl().toString();

    Request.Builder requestBuilder = originalRequest.newBuilder()
        //Basic Authentication,也可用於token驗證,OAuth驗證
        .header("Authorization", basic)
        .header("Accept", "application/json")
        .method(originalRequest.method(), originalRequest.body());

    Request request = requestBuilder.build();

    Response originalResponse = chain.proceed(request);
    Response.Builder responseBuilder =
        //Cache control設定快取
        originalResponse.newBuilder().header("Cache-Control", cacheControl);

    return responseBuilder.build();
  }
}

快取策略

一般情況下我們需要達到的快取效果是這樣的:

  • 沒有網或者網路較差的時候要使用快取(統一設定)
  • 有網路的時候,要保證不同的需求,實時性資料不用快取,一般請求需要快取(單個請求的header來實現)。

OkHttp3中有一個Cache類是用來定義快取的,此類詳細介紹了幾種快取策略,具體可看此類原始碼。

noCache :不使用快取,全部走網路
noStore : 不使用快取,也不儲存快取
onlyIfCached : 只使用快取
maxAge :設定最大失效時間,失效則不使用
maxStale :設定最大失效時間,失效則不使用
minFresh :設定最小有效時間,失效則不使用
FORCE_NETWORK : 強制走網路
FORCE_CACHE :強制走快取

配置目錄

這個是快取檔案的存放位置,okhttp預設是沒有快取,且沒有快取目錄的。

 private static final int HTTP_RESPONSE_DISK_CACHE_MAX_SIZE = 10 * 1024 * 1024;

  private Cache cache() {
         //設定快取路徑
         final File baseDir = AppUtil.getAvailableCacheDir(sContext);
         final File cacheDir = new File(baseDir, "HttpResponseCache");
         //設定快取 10M
         return new Cache(cacheDir, HTTP_RESPONSE_DISK_CACHE_MAX_SIZE);
     }

其中獲取cacahe目錄,我們一般採取的策略就是應用解除安裝,即刪除。一般就使用如下兩個目錄:

  • data/$packageName/cache:Context.getCacheDir()
  • /storage/sdcard0/Andorid/data/$packageName/cache:Context.getExternalCacheDir()

且當sd卡空間小於data可用空間時,使用data目錄。

 /**
     * |   ($rootDir)
     * +- /data                    -> Environment.getDataDirectory()
     * |   |
     * |   |   ($appDataDir)
     * |   +- data/$packageName
     * |       |
     * |       |   ($filesDir)
     * |       +- files            -> Context.getFilesDir() / Context.getFileStreamPath("")
     * |       |      |
     * |       |      +- file1     -> Context.getFileStreamPath("file1")
     * |       |
     * |       |   ($cacheDir)
     * |       +- cache            -> Context.getCacheDir()
     * |       |
     * |       +- app_$name        ->(Context.getDir(String name, int mode)
     * |
     * |   ($rootDir)
     * +- /storage/sdcard0         -> Environment.getExternalStorageDirectory()/ Environment.getExternalStoragePublicDirectory("")
     * |                 |
     * |                 +- dir1   -> Environment.getExternalStoragePublicDirectory("dir1")
     * |                 |
     * |                 |   ($appDataDir)
     * |                 +- Andorid/data/$packageName
     * |                                         |
     * |                                         | ($filesDir)
     * |                                         +- files                  -> Context.getExternalFilesDir("")
     * |                                         |    |
     * |                                         |    +- file1             -> Context.getExternalFilesDir("file1")
     * |                                         |    +- Music             -> Context.getExternalFilesDir(Environment.Music);
     * |                                         |    +- Picture           -> Context.getExternalFilesDir(Environment.Picture);
     * |                                         |    +- ...               -> Context.getExternalFilesDir(String type)
     * |                                         |
     * |                                         |  ($cacheDir)
     * |                                         +- cache                  -> Context.getExternalCacheDir()
     * |                                         |
     * |                                         +- ???
     * <p/>
     * <p/>
     * 1.  其中$appDataDir中的資料,在app解除安裝之後,會被系統刪除。
     * <p/>
     * 2.  $appDataDir下的$cacheDir:
     * Context.getCacheDir():機身記憶體不足時,檔案會被刪除
     * Context.getExternalCacheDir():空間不足時,檔案不會實時被刪除,可能返回空物件,Context.getExternalFilesDir("")亦同
     * <p/>
     * 3. 內部儲存中的$appDataDir是安全的,只有本應用可訪問
     * 外部儲存中的$appDataDir其他應用也可訪問,但是$filesDir中的媒體檔案,不會被當做媒體掃描出來,加到媒體庫中。
     * <p/>
     * 4. 在內部儲存中:通過  Context.getDir(String name, int mode) 可獲取和  $filesDir  /  $cacheDir 同級的目錄
     * 命名規則:app_ + name,通過Mode控制目錄是私有還是共享
     * <p/>
     * <code>
     * Context.getDir("dir1", MODE_PRIVATE):
     * Context.getDir: /data/data/$packageName/app_dir1
     * </code>
     */

快取第一種型別

配置單個請求的@Headers,設定此請求的快取策略,不影響其他請求的快取策略,不設定則沒有快取。

// 設定 單個請求的 快取時間
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

快取第二種型別

有網和沒網都先讀快取,統一快取策略,降低伺服器壓力。

private Interceptor cacheInterceptor() {
      Interceptor cacheInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response response = chain.proceed(request);

                String cacheControl = request.cacheControl().toString();
                if (TextUtils.isEmpty(cacheControl)) {
                    cacheControl = "public, max-age=60";
                }
                return response.newBuilder()
                        .header("Cache-Control", cacheControl)
                        .removeHeader("Pragma")
                        .build();
            }
        };
      }

快取第三種類型

結合前兩種,離線讀取本地快取,線上獲取最新資料(讀取單個請求的請求頭,亦可統一設定)。

private Interceptor cacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();

                if (!AppUtil.isNetworkReachable(sContext)) {
                    request = request.newBuilder()
                            //強制使用快取
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                Response response = chain.proceed(request);

                if (AppUtil.isNetworkReachable(sContext)) {
                    //有網的時候讀介面上的@Headers裡的配置,你可以在這裡進行統一的設定
                    String cacheControl = request.cacheControl().toString();
                    Logger.i("has network ,cacheControl=" + cacheControl);
                    return response.newBuilder()
                            .header("Cache-Control", cacheControl)
                            .removeHeader("Pragma")
                            .build();
                } else {
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    Logger.i("network error ,maxStale="+maxStale);
                    return response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)
                            .removeHeader("Pragma")
                            .build();
                }

            }
        };
    }

錯誤處理

在請求網路的時候,我們不止會得到HttpException,還有我們和伺服器約定的errorCode和errorMessage,為了統一處理,我們可以
預處理以下上面兩個欄位,定義BaseModel,在ConverterFactory中進行處理,
可參照:

網路狀態監聽

一般在沒有網路的時候使用快取資料,有網路的時候及時重試獲取最新資料,其中獲取是否有網路,我們採用廣播的形式:

 public class NetWorkReceiver extends BroadcastReceiver {

     @Override
     public void onReceive(Context context, Intent intent) {
         HttpNetUtil.INSTANCE.setConnected(context);
     }
 }

HttpNetUtil實時獲取網路連線狀態,關鍵程式碼

   /**
     * 獲取是否連線
     */
    public boolean isConnected() {
        return isConnected;
    }
   /**
     * 判斷網路連線是否存在
     *
     * @param context
     */
    public void setConnected(Context context) {
        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (manager == null) {
            setConnected(false);


            if (networkreceivers != null) {
                for (int i = 0, z = networkreceivers.size(); i < z; i++) {
                    Networkreceiver listener = networkreceivers.get(i);
                    if (listener != null) {
                        listener.onConnected(false);
                    }
                }
            }

        }

        NetworkInfo info = manager.getActiveNetworkInfo();

        boolean connected = info != null && info.isConnected();
        setConnected(connected);

        if (networkreceivers != null) {
            for (int i = 0, z = networkreceivers.size(); i < z; i++) {
                Networkreceiver listener = networkreceivers.get(i);
                if (listener != null) {
                    listener.onConnected(connected);
                }
            }
        }

    }

在需要監聽網路的介面或者base(需要判斷當前activity是否在棧頂)實現Networkreceiver。

Retrofit封裝

全域性單利的OkHttpClient:

okHttp() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        okHttpClient = new OkHttpClient.Builder()
                //列印日誌
                .addInterceptor(interceptor)

                //設定Cache目錄
                .cache(CacheUtil.getCache(UIUtil.getContext()))

                //設定快取
                .addInterceptor(cacheInterceptor)
                .addNetworkInterceptor(cacheInterceptor)

                //失敗重連
                .retryOnConnectionFailure(true)

                //time out
                .readTimeout(TIMEOUT_READ, TimeUnit.SECONDS)
                .connectTimeout(TIMEOUT_CONNECTION, TimeUnit.SECONDS)

                .build()

        ;
    }

全域性單利的Retrofit.Builder,這裡返回builder是為了方便我們設定baseUrl的,我們可以動態建立多個api介面,當然也可以用@Url註解

Retrofit2Client() {
        retrofitBuilder = new Retrofit.Builder()
                //設定OKHttpClient
                .client(okHttp.INSTANCE.getOkHttpClient())

                //Rx
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())

                //String轉換器
                .addConverterFactory(StringConverterFactory.create())

                //gson轉化器
                .addConverterFactory(GsonConverterFactory.create())
        ;
    }

Retrofit2+RxJava 使用Demo:Retrofit2Demo