Retrofit用法

一、 简介

随着Google对HttpClient 摒弃,和Volley的逐渐没落,OkHttp开始异军突起,而Retrofit则对okHttp进行了强制依赖。

Retrofit是Square公司开发的一款针对Android网络请求的框架,Retrofit2底层基于OkHttp实现的,OkHttp现在已经得到Google官方认可,大量的app都采用OkHttp做网络请求,其源码详见OkHttp Github。

如果看源码会发现其实质上就是对okHttp的封装,使用面向接口的方式进行网络请求,利用动态生成的代理类封装了网络接口请求的底层,

其将请求返回javaBean,对网络认证 REST API进行了很好对支持此,使用Retrofit将会极大的提高我们应用的网络体验。

REST

既然是RESTful架构,那么我们就来看一下什么是REST吧。
REST(REpresentational State Transfer)是一组架构约束条件和原则。
RESTful架构都满足以下规则:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

更多关于REST的介绍:什么是REST - GitHub讲解的非常详细

首先先来看一个完整Get请求是如何实现:

  1. 创建业务请求接口,具体代码如下:

    public interface BlueService {@GET("book/search")Call<BookSearchResponse> getSearchBooks(@Query("q") String name, @Query("tag") String tag, @Query("start") int start, @Query("count") int count);
    }
    

    这里需要稍作说明,@GET注解就表示get请求,@Query表示请求参数,将会以key=value的方式拼接在url后面

  2. 需要创建一个Retrofit的示例,并完成相应的配置

    Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.douban.com/v2/").addConverterFactory(GsonConverterFactory.create()).build();BlueService service = retrofit.create(BlueService.class);

    这里的baseUrl就是网络请求URL相对固定的地址,一般包括请求协议(如Http)、域名或IP地址、端口号等,当然还会有很多其他的配置,下文会详细介绍。还有addConverterFactory方法表示需要用什么转换器来解析返回值,GsonConverterFactory.create()表示调用Gson库来解析json返回值,具体的下文还会做详细介绍。

  3. 调用请求方法,并得到Call实例

    Call<BookSearchResponse> call = mBlueService.getSearchBooks("小王子", "", 0, 3);
    

    Call其实在Retrofit中就是行使网络请求并处理返回值的类,调用的时候会把需要拼接的参数传递进去,此处最后得到的url完整地址为

    https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&tag=&start=0&count=3

  4. 使用Call实例完成同步或异步请求

    • 同步请求

      BookSearchResponse response = call.execute().body();

      这里需要注意的是网络请求一定要在子线程中完成,不能直接在UI线程执行,不然会crash

    • 异步请求

      call.enqueue(new Callback<BookSearchResponse>() {
      @Override
      public void onResponse(Call<BookSearchResponse> call,        Response<BookSearchResponse> response) {
      asyncText.setText("异步请求结果: " + response.body().books.get(0).altTitle);
      }
      @Override
      public void onFailure(Call<BookSearchResponse> call, Throwable t) {}
      });
      

二、如何使用

首先需要在build.gradle文件中引入需要的第三包,配置如下:

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'

引入完第三包接下来就可以使用Retrofit来进行网络请求了。接下来会对不同的请求方式做进一步的说明。

Get方法

1. @Query

Get方法请求参数都会以key=value的方式拼接在url后面,Retrofit提供了两种方式设置请求参数。第一种就是像上文提到的直接在interface中添加@Query注解,还有一种方式是通过Interceptor实现,直接看如何通过Interceptor实现请求参数的添加。

public class CustomInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {Request request = chain.request();HttpUrl httpUrl = request.url().newBuilder().addQueryParameter("token", "tokenValue").build();request = request.newBuilder().url(httpUrl).build();return chain.proceed(request);}
}
  • 11

addQueryParameter就是添加请求参数的具体代码,这种方式比较适用于所有的请求都需要添加的参数,一般现在的网络请求都会添加token作为用户标识,那么这种方式就比较适合。

创建完成自定义的Interceptor后,还需要在Retrofit创建client处完成添加

addInterceptor(new CustomInterceptor())
2. @QueryMap

如果Query参数比较多,那么可以通过@QueryMap方式将所有的参数集成在一个Map统一传递,还以上文中的get请求方法为例

public interface BlueService {@GET("book/search")Call<BookSearchResponse> getSearchBooks(@QueryMap Map<String, String> options);
}

调用的时候将所有的参数集合在统一的map中即可

Map<String, String> options = new HashMap<>();
map.put("q", "小王子");
map.put("tag", null);
map.put("start", "0");
map.put("count", "3");
Call<BookSearchResponse> call = mBlueService.getSearchBooks(options);
3. Query集合

假如你需要添加相同Key值,但是value却有多个的情况,一种方式是添加多个@Query参数,还有一种简便的方式是将所有的value放置在列表中,然后在同一个@Query下完成添加,实例代码如下:

public interface BlueService {@GET("book/search")Call<BookSearchResponse> getSearchBooks(@Query("q") List<String> name);
}

最后得到的url地址为

https://api.douban.com/v2/book/search?q=leadership&q=beyond%20feelings
4. Query非必填

如果请求参数为非必填,也就是说即使不传该参数,服务端也可以正常解析,那么如何实现呢?其实也很简单,请求方法定义处还是需要完整的Query注解,某次请求如果不需要传该参数的话,只需填充null即可。

针对文章开头提到的get的请求,加入按以下方式调用

Call<BookSearchResponse> call = mBlueService.getSearchBooks("小王子", null, 0, 3);

那么得到的url地址为

https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&start=0&count=3
5. @Path

如果请求的相对地址也是需要调用方传递,那么可以使用@Path注解,示例代码如下:

@GET("book/{id}")
Call<BookResponse> getBook(@Path("id") String id);

业务方想要在地址后面拼接书籍id,那么通过Path注解可以在具体的调用场景中动态传递,具体的调用方式如下:

 Call<BookResponse> call = mBlueService.getBook("1003078");

此时的url地址为

https://api.douban.com/v2/book/1003078

@Path可以用于任何请求方式,包括Post,Put,Delete等等

Post请求

1. @field

Post请求需要把请求参数放置在请求体中,而非拼接在url后面,先来看一个简单的例子

 @FormUrlEncoded@POST("book/reviews")Call<String> addReviews(@Field("book") String bookId, @Field("title") String title,@Field("content") String content, @Field("rating") String rating);

这里有几点需要说明的

  • @FormUrlEncoded将会自动将请求参数的类型调整为application/x-www-form-urlencoded,假如content传递的参数为Good Luck,那么最后得到的请求体就是

    content=Good+Luck

    FormUrlEncoded不能用于Get请求

  • @Field注解将每一个请求参数都存放至请求体中,还可以添加encoded参数,该参数为boolean型,具体的用法为

    @Field(value = "book", encoded = true) String book

    encoded参数为true的话,key-value-pair将会被编码,即将中文和特殊字符进行编码转换

2. @FieldMap

上述Post请求有4个请求参数,假如说有更多的请求参数,那么通过一个一个的参数传递就显得很麻烦而且容易出错,这个时候就可以用FieldMap

 @FormUrlEncoded@POST("book/reviews")Call<String> addReviews(@FieldMap Map<String, String> fields);
3. @Body

如果Post请求参数有多个,那么统一封装到类中应该会更好,这样维护起来会非常方便

@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@Body Reviews reviews);public class Reviews {public String book;public String title;public String content;public String rating;
}

其他请求方式

除了Get和Post请求,Http请求还包括Put,Delete等等,用法和Post相似,所以就不再单独介绍了。

上传

上传因为需要用到Multipart,所以需要单独拿出来介绍,先看一个具体上传的例子

首先还是需要新建一个interface用于定义上传方法

public interface FileUploadService {  // 上传单个文件@Multipart@POST("upload")Call<ResponseBody> uploadFile(@Part("description") RequestBody description,@Part MultipartBody.Part file);// 上传多个文件@Multipart@POST("upload")Call<ResponseBody> uploadMultipleFiles(@Part("description") RequestBody description,@Part MultipartBody.Part file1,@Part MultipartBody.Part file2);
}接下来我们还需要在Activity和Fragment中实现两个工具方法,代码如下:
public static final String MULTIPART_FORM_DATA = "multipart/form-data";@NonNull
private RequestBody createPartFromString(String descriptionString) {  return RequestBody.create(MediaType.parse(MULTIPART_FORM_DATA), descriptionString);
}@NonNull
private MultipartBody.Part prepareFilePart(String partName, Uri fileUri) {  File file = FileUtils.getFile(this, fileUri);// 为file建立RequestBody实例RequestBody requestFile =RequestBody.create(MediaType.parse(MULTIPART_FORM_DATA), file);// MultipartBody.Part借助文件名完成最终的上传return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

好了,接下来就是最终的上传文件代码了

Uri file1Uri = ... // 从文件选择器或者摄像头中获取
Uri file2Uri = ... // 创建上传的service实例
FileUploadService service =  ServiceGenerator.createService(FileUploadService.class);// 创建文件的part (photo, video, ...)
MultipartBody.Part body1 = prepareFilePart("video", file1Uri);
MultipartBody.Part body2 = prepareFilePart("thumbnail", file2Uri);// 添加其他的part
RequestBody description = createPartFromString("hello, this is description speaking");// 最后执行异步请求操作
Call<ResponseBody> call = service.uploadMultipleFiles(description, body1, body2);
call.enqueue(new Callback<ResponseBody>() {  @Overridepublic void onResponse(Call<ResponseBody> call,Response<ResponseBody> response) {Log.v("Upload", "success");}@Overridepublic void onFailure(Call<ResponseBody> call, Throwable t) {Log.e("Upload error:", t.getMessage());}
});

三、其他必须知道的事项

1. 添加自定义的header

Retrofit提供了两个方式定义Http请求头参数:静态方法和动态方法,静态方法不能随不同的请求进行变化,头部信息在初始化的时候就固定了。而动态方法则必须为每个请求都要单独设置。

  • 静态方法

    public interface BlueService {@Headers("Cache-Control: max-age=640000")@GET("book/search")Call<BookSearchResponse> getSearchBooks(@Query("q") String name, @Query("tag") String tag, @Query("start") int start, @Query("count") int count);
    }

    当然你想添加多个header参数也是可以的,写法也很简单

    public interface BlueService {@Headers({"Accept: application/vnd.yourapi.v1.full+json","User-Agent: Your-App-Name"})@GET("book/search")Call<BookSearchResponse> getSearchBooks(@Query("q") String name, @Query("tag") String tag, @Query("start") int start, @Query("count") int count);
    }

    此外也可以通过Interceptor来定义静态请求头

    public class RequestInterceptor implements Interceptor {@Overridepublic Response intercept(Chain chain) throws IOException {Request original = chain.request();Request request = original.newBuilder().header("User-Agent", "Your-App-Name").header("Accept", "application/vnd.yourapi.v1.full+json").method(original.method(), original.body()).build();return chain.proceed(request);}
    }

    添加header参数Request提供了两个方法,一个是header(key, value),另一个是.addHeader(key, value),两者的区别是,header()如果有重名的将会覆盖,而addHeader()允许相同key值的header存在

    然后在OkHttp创建Client实例时,添加RequestInterceptor即可

    private static OkHttpClient getNewClient(){
    return new OkHttpClient.Builder().addInterceptor(new RequestInterceptor()).connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS).build();
    }
    
  • 动态方法

    public interface BlueService {@GET("book/search")Call<BookSearchResponse> getSearchBooks(@Header("Content-Range") String contentRange, @Query("q") String name, @Query("tag") String tag, @Query("start") int start, @Query("count") int count);
    }
    

2. 网络请求日志

调试网络请求的时候经常需要关注一下请求参数和返回值,以便判断和定位问题出在哪里,Retrofit官方提供了一个很方便查看日志的Interceptor,你可以控制你需要的打印信息类型,使用方法也很简单。

首先需要在build.gradle文件中引入logging-interceptor

compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
  • 1
  • 1

同上文提到的CustomInterceptor和RequestInterceptor一样,添加到OkHttpClient创建处即可,完整的示例代码如下:

private static OkHttpClient getNewClient(){HttpLoggingInterceptor logging = new HttpLoggingInterceptor();logging.setLevel(HttpLoggingInterceptor.Level.BODY);return new OkHttpClient.Builder().addInterceptor(new CustomInterceptor()).addInterceptor(logging).connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS).build();
}

HttpLoggingInterceptor提供了4中控制打印信息类型的等级,分别是NONE,BASIC,HEADERS,BODY,接下来分别来说一下相应的打印信息类型。

  • NONE

    没有任何日志信息

  • Basic

    打印请求类型,URL,请求体大小,返回值状态以及返回值的大小

    D/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body)
    D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)  
    
  • Headers

    打印返回请求和返回值的头部信息,请求类型,URL以及返回值状态码

  • 打印请求和返回值的头部和body信息

3. 为某个请求设置完整的URL

​ 假如说你的某一个请求不是以base_url开头该怎么办呢?别着急,办法很简单,看下面这个例子你就懂了

public interface BlueService {  @GETpublic Call<ResponseBody> profilePicture(@Url String url);
}Retrofit retrofit = Retrofit.Builder()  .baseUrl("https://your.api.url/");.build();BlueService service = retrofit.create(BlueService.class);
service.profilePicture("https://s3.amazon.com/profile-picture/path");

​ 直接用@Url注解的方式传递完整的url地址即可。

4. 取消请求

Call提供了cancel方法可以取消请求,前提是该请求还没有执行

String fileUrl = "http://futurestud.io/test.mp4";
Call<ResponseBody> call =  downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {  @Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {Log.d(TAG, "request success");}@Overridepublic void onFailure(Call<ResponseBody> call, Throwable t) {if (call.isCanceled()) {Log.e(TAG, "request was cancelled");} else {Log.e(TAG, "other larger issue, i.e. no network connection?");}}
});}// 触发某个动作,例如用户点击了取消请求的按钮
call.cancel();
}

-----------------------------------------------提高-----------------------------------

retrofit注解:

方法注解,包含@GET、@POST、@PUT、@DELETE、@PATH、@HEAD、@OPTIONS、@HTTP。
标记注解,包含@FormUrlEncoded、@Multipart、@Streaming。
参数注解,包含@Query,@QueryMap、@Body、@Field,@FieldMap、@Part,@PartMap。

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

<font color=#DC143C size=5>几个特殊的注解</font>
@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();

<font color=#DC143C size=5>常用注解</font>
@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 uriFile file = FileUtils.getFile(this, fileUri);// create RequestBody instance from fileRequestBody requestFile =RequestBody.create(MediaType.parse("multipart/form-data"), file);// MultipartBody.Part is used to send also the actual file nameMultipartBody.Part body =MultipartBody.Part.createFormData("picture", file.getName(), requestFile);// add another part within the multipart requestString descriptionString = "hello, this is description speaking";RequestBody description =RequestBody.create(MediaType.parse("multipart/form-data"), descriptionString);

参考: Retrofit2 完全解析 探索与okhttp之间的关系
Retrofit 2 — How to Upload Files to Server

@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,表示不处理@Overridepublic 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*/@Overridepublic 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;

StringConverterFactory实现源码

自定义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();}
}

缓存策略

设置缓存就需要用到OkHttp的interceptors,缓存的设置需要靠请求和响应头。
如果想要弄清楚缓存机制,则需要了解一下HTTP语义,其中控制缓存的就是Cache-Control字段
参考:Retrofit2.0+okhttp3缓存机制以及遇到的问题
How Retrofit with OKHttp use cache data when offline
使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求

一般情况下我们需要达到的缓存效果是这样的:

  • 没有网或者网络较差的时候要使用缓存(统一设置)
  • 有网络的时候,要保证不同的需求,实时性数据不用缓存,一般请求需要缓存(单个请求的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");//设置缓存 10Mreturn 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目录。

最后来一张图看懂Android内存结构,参考:Android文件存储使用参考 - liaohuqiu

 /*** |   ($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() {@Overridepublic 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();}};}

此中方式的缓存Interceptor实现:ForceCachedInterceptor.java

缓存第三种类型

结合前两种,离线读取本地缓存,在线获取最新数据(读取单个请求的请求头,亦可统一设置)。

private Interceptor cacheInterceptor() {return new Interceptor() {@Overridepublic 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 staleLogger.i("network error ,maxStale="+maxStale);return response.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale="+maxStale).removeHeader("Pragma").build();}}};}

此中方式的缓存Interceptor实现:OfflineCacheControlInterceptor.java

错误处理

在请求网络的时候,我们不止会得到HttpException,还有我们和服务器约定的errorCode和errorMessage,为了统一处理,我们可以
预处理以下上面两个字段,定义BaseModel,在ConverterFactory中进行处理,
可参照:

  • Retrofit+RxJava实战日志(3)-网络异常处理
  • retrofit-2-simple-error-handling

网络状态监听

一般在没有网络的时候使用缓存数据,有网络的时候及时重试获取最新数据,其中获取是否有网络,我们采用广播的形式:

 public class NetWorkReceiver extends BroadcastReceiver {@Overridepublic 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());}


参考:
Articles tagged in: Retrofit

官方文档

Retrofit2 完全解析 探索与okhttp之间的关系

Retrofit 2.0 + OkHttp 3.0 配置

更新到Retrofit2的一些技巧

Effective OkHttp

Okhttp-wiki 之 Interceptors 拦截器

Retrofit2.0+okhttp3缓存机制以及遇到的问题

How Retrofit with OKHttp use cache data when offline

使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网根据过期时间重新请求

用 Retrofit 2 简化 HTTP 请求

Retrofit请求参数注解字段说明

Android文件存储使用参考 - liaohuqiu

Retrofit+RxJava实战日志(3)-网络异常处理

retrofit-2-simple-error-handling

图解 Retrofit - ServiceMethod

关于Retrofit用法相关推荐

  1. Android Retrofit★

    1.Retrofit Retrofit是基于OkHttp封装的网络请求框架,网络请求的工作本质上是OkHttp完成的,而Retrofit仅负责网络请求接口的封装. OkHttp的缺点: ①配置网络请求 ...

  2. Retrofit2 完全解析 探索与okhttp之间的关系

    http://blog.csdn.net/lmj623565791/article/details/51304204: 本文出自:[张鸿洋的博客] 1. 概述 之前写了个okhttputils的工具类 ...

  3. android广告页白屏_年度整理!2056页《大厂安卓岗面试真题解析合集》火爆全网...

    前言 2020年还有最后一个月就结束了,时间一眨眼就过去了.今年面试有没有被面试官虐呢,明年跳槽想跳去哪个大厂呢,这是个问题.说实话,今年我面试也被虐了,为了明年能找到一份心怡的工作,特地的从朋友那里 ...

  4. 【Other】最近在研究的, Java/Springboot/RPC/JPA等

    我的Springboot框架,欢迎关注: https://github.com/junneyang/common-web-starter Dubbo-大波-服务化框架 dubbo_百度搜索Dubbo与 ...

  5. Retrofit2源码解析

    一.前言: Retrofit: 一个 Restful 设计风格的 HTTP 网络请求框架的封装.基于 OkHttp 严格地说,Retrofit2并不是一个网络请求交易框架,它只是对网络请求框架的封装. ...

  6. Retrofit-原理全解析

    1.Retrofit介绍 Retrofit是一个RESTful的Http网络请求框架的封装,网络请求部分本质是由OKHttp完成的 而我们学习Retrofit除了对Http请求有进一步的了解之外,我们 ...

  7. retrofit框架学习(一)----基本用法

    首先公布下我学习retrofit的思维导图 如果大家有需要这个思维导图,可在下面留言,我看到后会分享给你. 前言 什么是retrofit? 官方的回答是: A type-safe HTTP clien ...

  8. Android网络开发之Retrofit+OkHttpClient+RXJava 基本用法演示

    写本文的目的只是为了给需要帮助的朋友提供一个演示,也是记录一下使用这些流行库的基本用法,以便日后用到的时候可以及时找到. 好了,进入正题,关于这三个库的介绍我就不说了,自己百度,先看程序执行效果图: ...

  9. Android网络请求开源框架retrofit的基本GET用法(2.4版本)

    1.先在gradle配置retrfit版本如下: //rxjava/retrofit相关引入compile 'io.reactivex.rxjava2:rxandroid:2.0.2'compile ...

最新文章

  1. BH60绝对位置旋转编码器编程资料
  2. 微软BI 之SSIS 系列 - 利用 SSIS 模板快速开发 SSIS Package
  3. 测试一下能够图文混排
  4. windows mobile开发循序渐进(1)关于平台和工具
  5. Intel汇编语言程序设计学习笔记1
  6. arcgis多个数据融合python_使用Python在ArcGIS中添加多个字段名
  7. Python:通过获取淘宝账号和密码的实验,来看登陆方式选择的重要性
  8. 2012年4月份第2周51Aspx源码发布详情
  9. 哪些Amazon erp是可以免费使用的?
  10. 服务器wifi模块通讯协议,WiFi模块 TCP/IP协议栈
  11. html吃豆豆小游戏源码,HTML5 Canvas吃豆豆动画
  12. 遥感影像、DEM免费下载以及MPT制作
  13. zeppelin部署安装
  14. 怎么把vue改写成html,vue将字符串转为为html
  15. html选区控制怎么用,ps载入选区的快捷键是什么?
  16. 盲盒源码开发附搭建教程
  17. B端页面——详细表格设计流程
  18. VGGNet网络结构
  19. (Python)正则表达式(二)
  20. word2019安装mythtype后出现“不受信任的位置及禁用宏”问题解决办法

热门文章

  1. Python编程:从入门到实践(美).pdf -学习篇(基础)
  2. OpenCVSharp学习(五):关于摄像头的操作,打开,拍照,录视频
  3. 更改极点为第一输入法
  4. 搜狗号码通与天翼开放平台达成合作 可识别号码扩容
  5. 机器学习(一) 机器学习概述
  6. 超融合环境,虚拟化环境使用首驰兴华USB服务器实现银企直联,首驰兴华USBserver
  7. google AdSense广告不显示的原因
  8. 营收、GMV飞升,有赞的亏损病仍然无解?
  9. kelvin 四线连接电阻测试法
  10. 【Planning】Firm type used in R3 Planning - Manual Firm and regular run