封装接口 首先,我们将将请求api封装成一个接口(interface
), Retrofit 通过这个定义的interface
生成一个具体的实现。 在interface
中进行接口api的定义,比如
1 2 3 4 public interface RepoService { @GET("/users/{user}/repos") Call<ResponseBody> listRepo(@Path("user") String user); }
构造Retrofit
接着,我们构造Retrofit
1 2 RepoService repoService = HttpUtil_Github.getInstance().create(RepoService.class); Call<ResponseBody> call = repoService.listRepo(user);
当调用 listRepo(String user)
这个方法时,传入的参数会填入到 注解中 {user}
中
HttpUtil_Github.getInstance
这个方法去获取 Retrofit 的一个单例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 private Retrofit retrofit; private static volatile HttpUtil_Github instance = null; public HttpUtil_Github() { retrofit = new Retrofit.Builder() .baseUrl(NetUtil.GITHUB) .addConverterFactory(GsonConverterFactory.create()) .build(); } //单例 public static HttpUtil_Github getInstance(){ HttpUtil_Github mInstance = instance; if (mInstance == null){ synchronized (HttpUtil_Github.class){ mInstance = instance; if (mInstance == null){ mInstance = new HttpUtil_Github(); instance = mInstance; } } } return mInstance; } public <T> T create(Class<T> service) { return retrofit.create(service); }
##发起请求
然后通过异步发起GET
请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 call.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { try { Log.d(TAG,""+call.request().url()); Log.d(TAG,response.body().string()); } catch (IOException e) { e.printStackTrace(); } } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { } });
在创建 Retrofit
对象时,传入我们的服务器的地址 baseUrl
,完整的请求路径就是通过这个baseUrl
和我们注解中的地址已经传入的参数结合起来的。
这里讲一下url的配置 分为几种情况
baseUrl
使用根域名形式
和 path
使用绝对路径形式
1 2 3 baseUrl: https://api.github.com path: /users/{user}/repos url: https://api.github.com/users/tingya/repos
baseUrl
使用目录形式
和 path
使用相对路径形式
1 2 3 baseUrl: https://api.github.com/ path: users/{user}/repos url: https://api.github.com/users/tingya/repos
或者
1 2 3 baseUrl: https://api.github.com/users/ path:{user}/repos url:https://api.github.com/users/tingya/repos
baseUrl
使用目录形式
和 path
使用绝对路径形式
1 2 3 baseUrl: https://api.github.com/users/ path: /{user}/repos url: https://api.github.com/tingya/repos
baseUrl
使用文件形式
和 path
使用相对路径形式
1 2 3 baseUrl:https://api.github.com/users path:{user}/repos url: https://api.github.com/tingya/repos
这种情况下会报错 error: baseUrl must end in /: https://api.github.com/users
path
使用完全路径
1 2 3 4 baseUrl: https://api.github.com/users/ path https://api.github.com/users/{user}/repos url: https://api.github.com/users/tingya/repos
当你使用完全路径时,Retrofit就会忽略掉你实例化它时通过baseUrl()方法传给它的host(接口前缀地址); 因为在实际项目中可能会有几个不同ip的host或者端口不同的host,通过这个方法可以避免实例化多个Retrofit
对象
上面是最简单的GET
请求的demo 下面来说说其他的请求方式以及参数的配置
GET方法 baseUrl 是 https://api.demo.com
@Path 这个注解用来替换相对路径中的值
1 2 @GET("/users/{user}/repos") Call<ResponseBody> getMessage(@Path("user") String user);
通过Call<ResponseBody> call = repoService.getMessage("name");
最终的url
等价于https://api.demo.com/user/name/repos
@Query 和 @QueryMap 这个注解是用来给GET
方法传参的,只能用于GET
方法
1 2 @GET("/users/name/repos") Call<ResponseBody> getMessage(@Query("age") int age);
通过Call<ResponseBody> call = repoService.getMessage(20);
最终的url等价于https://api.demo.com/user/name/repos?age=20
当有N个参数时,我们总不能说去写N个@Query
吧,如果遇到有些情况下只需要部分参数的时候,我们就得再去写一个interface
,使用不同数量的参数,这样不利于我们代码的解耦 这个时候,我们应该使用 @QueryMap
这个注解
1 2 @GET("/users/name/repos") Call<ResponseBody> getMessage(@QueryMap Map<String,Object> map)
通过
1 2 3 4 Map<String,Object> map = new HashMap(); map.put("age",20); map.put("sex","famale"); Call<ResponseBody> call = repoService.getMessage(map);
最终的url等价于https://api.demo.com/user/name/repos?age=20&sex=famale
POST方法 @Field 通过post方法提交表单
1 2 @Post("/user/name/repos") Call<ResponseBody> getMessage(@Field("age") int age );
通过Call<ResponseBody> call = repoService.getMessage(20)
会往http请求的body
中添加表单age=20
@FieldMap 当需要通过POST方法
提交多个key=value时候,可以通过@FieldMap
这个注解提交多个参数
1 2 @POST("") Call<ResponseBody> getMessage(@FieldMap Map<String,Object> map)
1 2 3 4 Map<String,Object> map = new HashMap(); map.put("age",20); map.put("sex","famale"); Call<ResponseBody> call = repoService.getMessage(map);
传json数据 比如需要传下面这样的json格式的数据
写一个 JavaBean,存放数据 比如
1 2 3 4 5 6 7 8 public class UserInfo { private String name; private int age; public String getName() { return name;} public void setName(String name) { this.name = name;} public int getAge() { return age;} public void setAge(int age) { this.age = age;} }
定义接口:
1 2 @POST("/user/name/repos") Call<ResponseBody> getMessage(@Body UserInfo userInfo);
通过下面的方法去调用接口
1 2 3 4 UserInfo userInfo = new UserInfo(); userInfo.setAge(20); userInfo.setName("user"); Call<ResponseBody> call = service.getMessage(userInfo);
传文件 定义接口
1 2 3 4 5 @Multipart //这个注解为http请求报文头添加 Content-Type: multipart/form-data; boundary=5b7b2ddf-bef2-4a32-ac21-e4662ea82771 //对应请求头 第一行 @POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar") Call<ResponseBody> upload(@Part("access_token") RequestBody token, //通过`@Part`这个注解,会帮我们在请求头中生成`Content-Disposition: form-data; name="access_token"`这样的请求格式 @Part MultipartBody.Part picture);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //设置 Content-Type RequestBody requestFile = RequestBody.create(MediaType.parse("image/png"), file); //设置 requestFile 的 Content-Disposition form-data; name="pic"; filename="icon_equipment.png"//`filename`是指想放在服务器的图片名字 MultipartBody.Part body = MultipartBody.Part.createFormData("pic", file.getName(), requestFile); //上面这行中,`createFormData(a,b,c)`中的第一个参数是传`name`,相当于`@Part("access_token") RequestBody token`中的`access_token`,所以一般不能使用`@Part("text") MultipartBody.Part picture`,因为在构建MultipartBody.Part时,已经包含了`picture`的name在里面,其实这并不能运行😄 UploadFileService uploadFileService = HttpUtil_Gank.getInstance().create(UploadFileService.class); Call<ResponseBody> call = uploadFileService.upload( RequestBody.create(MediaType.parse("multipart/form-data"), "token value jai485789hqn485yhhwb "),//携带的文字信息 body);
下面是上面的请求组成的上传文件的http Post请求头 其中boundary
指的是分隔符,用来分割不同的请求部分
可以很容易看出,这个请求体是多个相同的部分组成的: 每一个部分都是以–-
加分隔符开始的,然后是该部分内容的描述信息(Content-Disposition
),然后一个回车,然后是描述信息的具体内容; 如果传送的内容是一个文件的话,那么还会包含文件名信息(Content-Disposition: form-data; name="pic"; filename="icon_equipment.png"
),以及文件内容的类型(Content-Type
)。下面的第三个小部分其实是一个文件体的结构,最后会以–分割符–结尾,表示请求体结束。 —- Android Retrofit 实现文字(参数)和多张图片一起上传
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 D/OkHttp: --> POST http://api.stay4it.com/v1/public/core/?service=user.updateAvatar http/1.1 D/OkHttp: Content-Type: multipart/form-data; boundary=ed6fdfed-e436-4096-a70a-3a7b9657c933 D/OkHttp: Content-Length: 1100 D/OkHttp: Host: api.stay4it.com D/OkHttp: Connection: Keep-Alive D/OkHttp: Accept-Encoding: gzip D/OkHttp: User-Agent: okhttp/3.4.1 D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933 D/OkHttp: Content-Disposition: form-data; name="access_token" D/OkHttp: Content-Transfer-Encoding: binary D/OkHttp: Content-Type: multipart/form-data; charset=utf-8 D/OkHttp: Content-Length: 33 D/OkHttp: token value jai485789hqn485yhhwb D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933 D/OkHttp: Content-Disposition: form-data; name="pic"; filename="icon_equipment.png" D/OkHttp: Content-Type: image/png D/OkHttp: Content-Length: 658 D/OkHttp: �PNG D/OkHttp: D/OkHttp: ������IHDR������F������F������F���������PLTE������|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|��|�������������䰺������������������⥰��~�����,) )������tRNS����b����V�_LG���/,���J������IDATXì�˒�0��N@P�֜�����pԲjR��̷`y�!�M���R�L�U���T��hY�H��8+���SP8�"�f�}j�Cr�H�"'�*z��,�r�+b����]Y`����S7�y�s�Q�����&d�Iڙ5F�[��/��0}=*'�Xe��hx��RxJE�Qo1Q �M��3�:�`��*cvp�|�ke�X�T]�8[�eo^��3���y��uݸf��>w�D�1��9>>h)I23�����r��I�g��b�(���@@%��4"��)�&����Uw~�p�u������G�-ZMD���"JGc����Ș �dL����C���DO������ B&���h"� �$r�,�ꈑk\���k _J�Y���*��U�Qܭ���������IEND�B`� D/OkHttp: --ed6fdfed-e436-4096-a70a-3a7b9657c933-- D/OkHttp: --> END POST (1100-byte body) D/OkHttp: <-- 200 OK http://api.stay4it.com/v1/public/core/?service=user.updateAvatar (63ms) D/OkHttp: Server: nginx/1.4.6 (Ubuntu) D/OkHttp: Date: Tue, 18 Oct 2016 02:05:47 GMT D/OkHttp: Content-Type: application/json D/OkHttp: X-Frame-Options: SAMEORIGIN D/OkHttp: Transfer-Encoding: chunked D/OkHttp: Proxy-Connection: Keep-alive 10-18 10:05:45.387 28938-1993/me.ppting.gank D/OkHttp: {"ret":200,"msg":"有心课堂,传递给你的不仅仅是技术✈️","data":[{"url":"uploads/icon_equipment.png","filename":"icon_equipment.png"}]} 10-18 10:05:45.387 28938-1993/me.ppting.gank D/OkHttp: <-- END HTTP (150-byte body)
如果是传多个文件,则不能像上面上传一个文件使用传MultipartBody.Part
对象的方法,而是使用@PartMap
这个注解,同理,传多个文件其实就是在请求头中添加多个由分隔符隔开的部分,每隔部分都需要有
1 2 Content-Disposition:form-data;name="pic";filename="filename" Content-Type: image/png
等信息
可以将接口中的第二个参数@Part
改为@PartMap Map<String, RequestBody> params
将name
和 filename
存到 params中的第一个参数中 定义接口
1 2 3 @Multipart //这个注解为http请求报文头添加 Content-Type: multipart/form-data; boundary=5b7b2ddf-bef2-4a32-ac21-e4662ea82771 @POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar") Call<ResponseBody> uploadMore(@PartMap Map<String, RequestBody> map);
通过下面的代码添加请求文件并调用接口
1 2 3 4 5 6 7 Map<String, RequestBody> map = new HashMap<>(); for (File file : fileList) { map.put(file.getName()+"\";filename=\""+file.getName(),RequestBody.create(MediaType.parse("image/png"),file)); } UploadMoreFileService uploadMoreFileService = HttpUtil_Gank.getInstance().create(UploadMoreFileService.class); Call<ResponseBody> call = uploadMoreFileService.uploadMore(map);
上面的map中的第一个参数是为了在Content-Disposition: form-data; name="pic"; filename="icon_equipment.png"
中拼接 name="name";
和filename="filename"
,当多文件上传时,多个文件的name对应的value应该设为不同的值,所以这里我们取了文件的名字作为name的value。 写到这里我也有点疑问,那为什么不能模仿只传一个文件的时候那样使用MultipartBody.Part
呢,于是我试了一下 定义接口
1 2 3 @Multipart @POST("http://api.stay4it.com/v1/public/core/?service=user.updateAvatar") Call<ResponseBody> uploadMore(@PartMap Map<String,MultipartBody.Part> map);
然后
1 2 3 4 5 6 7 8 9 10 11 12 13 UploadMoreFileService uploadMoreFileService = HttpUtil_Gank.getInstance().create(UploadMoreFileService.class); RequestBody requestFile1 = RequestBody.create(MediaType.parse("image/png"),firstFile); RequestBody requestFile2 = RequestBody.create(MediaType.parse("image/png"),secondFile); MultipartBody.Part part1 = MultipartBody.Part.createFormData("pic", firstFile.getName(), requestFile1); MultipartBody.Part part2 = MultipartBody.Part.createFormData("pic", secondFile.getName(), requestFile2); Map<String, MultipartBody.Part> map = new HashMap<>(); map.put("",part1); map.put("",part2); Call<ResponseBody> call = uploadMoreFileService.uploadMore(map);
但是发现会报错@PartMap values cannot be MultipartBody.Part. Use @Part List<Part> or a different value type instead. (parameter #1)
添加拦截器 1 2 3 4 5 6 7 8 9 10 11 12 Request original = chain.request(); HttpUrl originalHttpUrl = original.url(); HttpUrl url = originalHttpUrl.newBuilder() .addQueryParameter("apikey", "your-actual-api-key") .build(); Request.Builder requestBuilder = original.newBuilder() .url(url); Request request = requestBuilder.build(); return chain.proceed(request);
自定义 Converter 建一个继承Converter.Factory的Factory类 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 public class StringConverterFactory extends Converter.Factory{ public static StringConverterFactory create() { return new StringConverterFactory(); } //{@link Body @Body}, {@link Part @Part}, and {@link PartMap @PartMap} //以上面这几个注解的request可以在这里拦截到并进行处理 而其他的则是用下面的方法 @Override public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { Log.d("StringConverterFactory"," type "+type.toString()); return super.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit); } @Override public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) { Log.d("StringConverterFactory","stringConverter"); return super.stringConverter(type, annotations, retrofit); } @Override public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { //如果不是我们想要处理的类型,则返回null,不进行处理 if (type != String.class){ return null; } return new StringResponseBodyConverter();//这里进行处理 } } public class StringResponseBodyConverter implements Converter<ResponseBody,String>{ //将 ResponseBody 转为 String @Override public String convert(ResponseBody value) throws IOException { Log.d("StringResponseBody ","StringResponseBody"); return value.string(); } }
Converter
是用来将ResponseBody
进行转化的我们需要的类CallAdapter
是用来转化返回类型的,比如将Call
转Observable
分享一个小tip 在QQ群里讨论Retrofit
的时候,别人分享的一个小技巧 当项目中的大多数接口需要token但有些不需要的时候,可以在@Headers
中定义Authorization
,并给其一个参数,例如true
or false
然后在Interceptor
中进行判断并进行处理 例如
1 2 3 @Headers({"\"Cache-Control\":max-age=0","Authorization:true"}) @GET("4/start-image/{width}*{height}") Observable<StartImageInfo> getSplashImage(@Path("width") int width, @Path("height") int height);
然后在自定义的Interceptor
中去取得Authorization
的值request.headers().get("Authorization")
,然后进行处理
参考文章
Retrofit使用指南 Android Retrofit 实现文字(参数)和多张图片一起上传 深入浅出 Retrofit,这么牛逼的框架你们还不来看看 Android Retrofit 2.0 使用-补充篇 Android 你必须了解的网络框架Retrofit2.0 Retrofit 2.0 超能实践(三),轻松实现多文件/图片上传