前言
前面说到了Retrofit
的一些简单的入门使用,如果还没入门的推荐先阅读这篇文章带你走进Retrofit的新世界,今天将直接以实践的方法来介绍Retrofit
原生的各方面的具体使用,有不足之处欢迎指出。下面将要介绍的内容主要为:
Retrofit
的封装
Converter
的使用
OkHttp
的Interceptor
的使用
url
的动态设置与运行时url
的设置
request
的重复使用与分析
Retrofit
的文件上传
Retrofit
的文件下载
Retrofit的封装
首先给一个常规的写法:
1 2 3 4 5 6
| public interface GithubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributors>> contributors( @Path("owner") String owner, @Path("repo") String repo); }
|
1 2 3 4 5 6 7
| Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(GsonConverterFactory.create()) .build(); GithubService service = retrofit.create(GithubService.class); Call<List<Contributors>> call = service.contributors("square", "retrofit");
|
我们的请求一般都很多,如果我们每次都换接口都要重复写Retrofit
,从而产生了累赘,我们可以定义一个通用的ServiceGenerator
,向其中传入需要调用的接口类,让其返回我们所需要的接口实例即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class ServiceGenerator { public static String API_BASE_URL = "https://api.github.com"; private ServiceGenerator() { } private static OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder(); private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_BASE_URL) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()); public static <S> S createService(Class<S> serviceClass) { Retrofit retrofit = builder.client(okHttpClient.build()).build(); return retrofit.create(serivceClass) } }
|
我们使用封装的ServiceGenerator
进行调用
1 2 3
| GithubService service = ServiceGenerator.createService(GithubService.class); Call<List<Contributors>> call = service.contributors("square", "retrofit");
|
是不是简单很多了呢,两行代码就搞定
Converter的使用
converter
的作用是对数据进行转化,默认Retrofit
的数据类型是OkHttp
的RequestBody
与ResponseBody
,但是我们在应用中会遇到许多不同的类型的数据,例如JSON
、XML
这时们就要使用到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
只需两步就能快速配置使用,例如Gson
1.在Gradle
中配置与Retrofit
对于的版本号
1
| compile 'com.squareup.retrofit2:converter-gson:2.1.0'
|
2.在Retrofit.Builder
中通过.addConverterFactory()
进行配置
1 2 3
| Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_DUOSHUO_URL) .addConverterFactory(GsonConverterFactory.create());
|
如果用多种不同的类型数据呢?例如Gson
、String
,放心也是通过.addConverterFactory()
进行添加.
1 2 3 4
| Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_DUOSHUO_URL) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create());
|
至于它们之间的冲突解决方案,Retrofit
会按照添加的顺序进行先后的匹配,直到找到相适合的数据类型。一般推荐将Gson
放到最后面。
OkHttp的Interceptor的使用
假设有这样一个情形:每个请求都要添加Connection
的请求头部,根据上篇文章的学习,代码如下:
1 2 3
| @Headers("Connection: keep-alive") @POST("/login") Call<String> loginService();
|
这样我们要为每一个请求都添加@Headers
字段或者在参数中添加@Header
,是不是有点多余与麻烦呢。因为Retrofit
网络请求处理是交给OkHttp
,所以我么可以对OkHttp
层进行请求信息的修改。下面我们使用Interceptor
就能完美的解决这个问题,调用OkHttp
的.addInterceptor()
方法,还是在ServiceGenerator
中修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static <S> S createService(Class<S> serviceClass) { okHttpClient.addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request();
Request.Builder builder = original.newBuilder() .addHeader("Connection", "keep-alive") .method(original.method(), original.body()); Request request = builder.build(); return chain.proceed(request); } }); Retrofit retrofit = builder.client(okHttpClient.build()).build(); return retrofit.create(serivceClass); }
|
通过chain.request()
获取原来的request
,再重新使用request.newBuilder()
方法构造新的Request.Builder
,这时可以通过.addHeader
或者.Header
方法来添加Header
。最后再构造新的request
调用chain.proceed(request)
。这里需要注意的是.addHeader
与.Header
的区别。
.addHeader
对相同的name
,不同value
不会进行覆盖,说明它可以存在多条相同name
的请求头
.Header
对相同name
会进行覆盖,说明对于相同name
的请求头只能存在一个
既然Interceptor
可以对header
进行统一设置,那么我们可以大胆猜想对于查询字段是否也可以统一设置呢,答案是肯定的,例如apikey
字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| okHttpClient.addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); //获取url HttpUrl originalUrl = original.url(); Request.Builder builder = original.newBuilder() .addHeader("Authorization", authToken) .method(original.method(), original.body()); //为url添加apikey字段 HttpUrl url = originalUrl.newBuilder() .addQueryParameter("apikey", "isdhh324bdsa") .build(); //将修改的url添加到新的Request中 Request request = builder .url(url) .build(); //执行 return chain.proceed(request); } }); }
|
url的动态设置与运行时url的设置
url的动态设置
如果我们的baseUrl
为https://github.com/api/v2
而设置的接口的设置如下
1 2 3 4
| public interface GithubService { @GET("repos") Call<List<Contributors>> contributors(); }
|
请求的url
为https://github.com/api/v2/repos
,如果我们现在要请求v3
版本的repos
呢?你会说很简单直接修改baseUrl
为https://github.com/api/v3
,但在前面我们已经在ServiceGenerator
中进行了封装baseUrl
所以我们不应该去修改它。这个时候就要用到/
,它能将host
后面的全部忽略,即baseUrl
直接成为https://github.com
,再在后面添加设置的endpoint url
。
1 2 3 4
| public interface GithubService { @GET("/api/v3/repos") Call<List<Contributors>> contributors(); }
|
这时请求的url
为https://github.com/api/v3/repos
,所以要慎重使用/
。
还有一种是使用//
,它的效果是对scheme
进行保持,例如baseUrl
为https://api.github.com
1 2 3 4 5 6 7 8 9
| public interface GithubService { //url为https://api.github.com/api/v3/repos @GET("/api/v3/repos") Call<List<Contributors>> contributors(); //url为https://api.github.com @GET("//api.github.com") Call<List<Contributors>> contributors(); }
|
运行时url的设置
在开发中可能会遇到这种情形:前面请求是url
为https://github.com/api/repos
但后面有一个请求的host
不同例如为https://duoshuo.com/api/repos
,但他们调用的都是同一个请求接口因为他们的endpoint
相同/api/repos
,这时我们只能改变请求的baseUrl
。对ServiceGenerator
添加新的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private static OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_DUOSHUO_URL) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()); //构造新的builder public static void changeApiBaseUrl(String newApiBaseUrl) { apiBaseUrl = newApiBaseUrl; builder = new Retrofit.Builder() .baseUrl(apiBaseUrl) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()); }
|
开始使用时调用changeApiBaseUrl
方法即可
1 2
| //改变baseUrl ServiceGenerator.changeApiBaseUrl("https://duoshuo.com");
|
还有一种情形:就是完全不相同的url
例如https://example.com/ioe/we
,这时我们可以使用另一个注释@Url
,该注释作用于参数字段,对请求时的url
动态设置
1 2
| @GET Call<List<Contributors>> contributors(@Url String url);
|
调用时直接传递url
参数即可
1 2
| GithubService service = ServiceGenerator.createService(GithubService.class); Call<List<Contributors>> call = service.contributors("https://example.com/ioe/we");
|
request的重复使用与分析
对同一个请求进行多次请求不能重复再调用它的Call
,可以通过调用它的clone
方法,复制一个新的Call
1 2 3 4 5 6 7 8 9 10
| GithubService service = ServiceGenerator.createService(GithubService.class); Call<List<Contributors>> call = service.contributors("square", "retrofit"); Callback<ResponseBody> callBack = new Callback<ResponseBody>() {...}; call.enqueue(callBack); //再一次请求 Call<ResponseBody> newCall = originalCall.clone(); newCall.enqueue(callBack);
|
同时可以调用Call.request
获取请求的request
,不管它是否执行完了还是未执行,还是在执行的时候调用了request.cacel()
取消了,都可以通过获取的request
查询请求的信息
1 2 3 4 5
| private void checkRequestContent(Request request) { Headers requestHeaders = request.headers(); RequestBody requestBody = request.body(); HttpUrl requestUrl = request.url(); }
|
Retrofit的文件上传
对于文件的上传Retrofit
可以通过@Multipart
进行标注来定义相关的上传接口,下面以多说的头像上传为例,首先来看下上传的数据格式,通过抓包分析



通过上面的信息可以知道请求的方式为post
,请求的url
为http://duoshuo.com/api/avatars/upload.json
,Content-Type
类型为multipart/form-data
这非常重要后续要设置数据的类型,请求的Disposition
分为两部分,其中一部分为RequestBody
类型的nonce
数据,另一部分为上传的图片信息,我们这部分以MultipartBody.Part
定义
1 2 3 4 5 6 7
| public interface DuoShuoUploadService { @Headers("Cookie: duoshuo_unique=your duoshuo cookie") @Multipart @POST("/api/avatars/upload.json") Call<ResponseBody> uploadService(@Part("nonce") RequestBody nonce, @Part MultipartBody.Part body); }
|
剩下的就是构造数据,进行调用请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public void getUpdateFile(File file, String nonceString) { // 创建上传的Service服务端 DuoShuoUploadService uploadService = ServiceGenerator.createService(DuoShuoUploadService.class); // 创建nonce数据的RequestBody RequestBody nonce = RequestBody.create(MediaType.parse("multipart/form-data"), nonceString); // 创建文件的RequestBody RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file); // 创建MultipartBody.Part MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); // 调用接口方法,并执行请求 Call<ResponseBody> call = uploadService.uploadService(nonce, body); call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { if (response.isSuccessful()) { Log.v("TAG", "success" + "->" + response.body()); } else { // APIError apiError = ErrorUtils.parseError(response); // Log.e("TAG", apiError.message()); } Log.d("TAG", " service contacted at:" + call.request().url()); } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { Log.e("TAG", "error:" + t.getMessage()); Log.d("TAG", " service contacted at:" + call.request().url()); } }); }
|
上面需要注意的有两点
- 在创建
RequestBody.create
时,其中的Content-Type
即MediaType
类型要设置为multipart/form-data
这是上传文件的类型。
MultipartBody.Part.createFormData()
的第一个参数为前面图中第二个Disposition
的name
字段的值,这个必须相同因为服务器会通过该字段获取数据,第二个参数就是文件名,第三个参数即为上传的图片数据RequestBody
类型。
对于多文件上传或者多个RequestBody
的字段,可以简单的通过传递多个MultipartBody.Part
类型的body
与@PartMap
来实现
1 2 3 4 5 6
| @Headers("Cookie: duoshuo_unique=your duoshuo cookie") @Multipart @POST("/api/avatars/upload.json") Call<ResponseBody> uploadMultipleService(@PartMap() Map<String, RequestBody> partMap, @Part MultipartBody.Part body1, @Part MultipartBody.Part body2);
|
如需求增加了,需要上传的进度条,这时可以继承RequestBody
来实现一个有进度条的上传,这里简单的贴下实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public class ProgressRequestBody extends RequestBody { public interface OkHttpProgressListener { void onProgress(long currentBytesCount, long totalBytesCount); } //实际的待包装请求体 private final RequestBody requestBody; //进度回调接口 private final OkHttpProgressListener progressListener; //包装完成的BufferedSink private BufferedSink bufferedSink; public ProgressRequestBody(RequestBody requestBody, OkHttpProgressListener progressListener) { this.requestBody = requestBody; this.progressListener = progressListener; } /** * 重写调用实际的响应体的contentType * @return MediaType */ @Override public MediaType contentType() { return requestBody.contentType(); } /** * 重写调用实际的响应体的contentLength * @return contentLength * @throws IOException 异常 */ @Override public long contentLength() throws IOException { return requestBody.contentLength(); } /** * 重写进行写入 * @param sink BufferedSink * @throws IOException 异常 */ @Override public void writeTo(BufferedSink sink) throws IOException { if (null == bufferedSink) { bufferedSink = Okio.buffer(sink(sink)); } requestBody.writeTo(bufferedSink); //必须调用flush,否则最后一部分数据可能不会被写入 bufferedSink.flush(); } /** * 写入,回调进度接口 * @param sink Sink * @return Sink */ private Sink sink(Sink sink) { return new ForwardingSink(sink) { //当前写入字节数 long writtenBytesCount = 0L; //总字节长度,避免多次调用contentLength()方法 long totalBytesCount = 0L; @Override public void write(Buffer source, long byteCount) throws IOException { super.write(source, byteCount); //增加当前写入的字节数 writtenBytesCount += byteCount; //获得contentLength的值,后续不再调用 if (totalBytesCount == 0) { totalBytesCount = contentLength(); } progressListener.onProgress(writtenBytesCount, totalBytesCount); } }; } }
|
Retrofit的文件下载
Retrofit
的文件下载也是很简单的,只要知道下载文件的url
,定制相应的接口方法,然后将响应的数据进行保存,直接上代码
1 2 3 4 5
| public interface DownloadFileService { @Streaming @GET Call<ResponseBody> downloadFileWithDynamicUrlSync(@Url String url); }
|
这里使用到了@Streaming
,如果不使用的话响应的数据是整个一次性保存,如果数据过大很容易产生OOM
现象;如果使用了响应的数据将会按流的形式分散传递,所以大文件建议使用@Streaming
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public void getDownloadFile(final String filePath) { final DownloadFileService downloadFileService = ServiceGenerator.createService(DownloadFileService.class); Call<ResponseBody> call = downloadFileService.downloadFileWithDynamicUrlSync(filePath);
call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { if (response.isSuccessful()) { boolean writeToDisk = WriteToDiskUtils.writeResponseBodyToDiskUtils(MainActivity.this, response.body()); Log.d("TAG", "下载成功?" + writeToDisk); } else { Log.d("TAG", "下载失败!"); } Log.d("TAG", " service contacted at:" + call.request().url()); } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { if (call.isCanceled()) { Log.d("TAG", "请求已经取消!"); } else { Log.e("TAG", t.getMessage()); } Log.d("TAG", " service contacted at:" + call.request().url()); } }); }
|
这里文件的保存使用了WriteToDiskUtils.writeResponseBodyToDiskUtils()
,其实就是一个简单的数据读取与写入,不多说了直接看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class WriteToDiskUtils { public static boolean writeResponseBodyToDiskUtils(Context context, ResponseBody body) { File file = new File(Environment.getExternalStorageDirectory() + File.separator + "retrofit/"); InputStream in = null; OutputStream out = null; long fileSize = body.contentLength(); int line; long downloadSize = 0; if (!file.exists()){ file.mkdirs(); } File realFile = new File(file,"vso.mp4"); try { in = body.byteStream(); out = new FileOutputStream(realFile); byte[] buffer = new byte[4096]; while ((line = in.read(buffer)) != -1) { out.write(buffer, 0, line); downloadSize += line; Log.d("TAG", "文件总的大小:" + fileSize + "文件下载的大小:" + downloadSize); } out.flush(); return true; } catch (IOException e) { e.printStackTrace(); return false; } finally { try { if (in != null) in.close(); if (out != null) out.close(); } catch (IOException e) { e.printStackTrace(); return false; } } } }
|
这次就到这里吧,去休息了。。。下次将对Retrofit
与RXJava
的结合使用进行介绍