利用Retrofit进行各种网络请求
目录
前言
关于Retrofit的优点,原理以及基本使用网上已经有很多相关的资料,但是自己实际使用的时候发现,比如多表单提交数据,比如上传动态数目图片等一些应用场景,官方文档以及网上并没有给出很好的解释和例子,在自己摸索一番后,特地记录下来并分享。
本文所用的一切代码都是继续Retrofit2.0
参考
Retrofit官网介绍了类型的网络请求的封装
Retrofit使用手册这里介绍了各种情况下使用Retrofit
Retrofit的基本使用
创建基本HttpClient
代码如下:
/**
* 请求超时时长
*/
private static final int API_BASE_URL = "http://www.hwits.top/";
/**
* 请求超时时长
*/
private static final int DEFAULT_TIMEOUT = 5;
/**
* OkHttpClient用来添加固定的请求HEAD
*/
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder().connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
/**
* 添加BaseUrl以及对Gson和Rxjava的依赖,每次创建
*/
private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create());
/**
* 不包含请求头的普通操作
*
* @param serviceClass
* @param <S>
* @return
*/
public <S> S createService(Class<S> serviceClass) {
//创建Retrofit客户端
OkHttpClient client = httpClient.build();
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}
简单的解释一下就是httpClient是一个OkHttpClient.Builder,设置了默认的请求时长。
builder是一个Retrofit.Builder,设置了请求的基地址以及使用Gson进行Json解析,使用Rxjava作为请求返回回调。
createService函数是用来创建请求接口服务的,用来具体实现请求
封装接口
以一个最简单的Get请求为例
/**
* 查询法律条文接口(异步)
*/
public interface TrafficLawService {
@GET("dictitem/getItemListByKind.json?kind=TrafficLaw")
Observable<HttpReply<ArrayList<Dictionaries>>> getTrafficLaw();
}
/**
* 查询法律条文接口(Sync)
*/
public interface TrafficLawService {
@GET("dictitem/getItemListByKind.json?kind=TrafficLaw")
HttpReply<ArrayList<Dictionaries>> getTrafficLaw();
}
简单解释一下就是,异步请求回返回给你一个回调这里使用Rxjava作为回调结果所以返回的是Observable,不设置回调的话返回为Call,Observable后面跟上的泛型就是你请求的返回结果通过序列化生成的实体,当然你也可以直接使用Retrofit提供的ResponseBody作为返回结果。
下面就是个阻塞的同步网络请求了,直接返回需要的结果
请求示例
Observable<HttpReply<ArrayList<Dictionaries>>> call =
client.createService(TrafficLawService).getTrafficLaw();
HttpReply<ArrayList<Dictionaries>> call =
client.createService(TrafficLawService).getTrafficLaw();
上面是异步调用返回的结果,后面是要经过Rxjava中的Subscriber进行订阅处理,这里不做详细解释
下面是直接返回结果,可以用来直接使用。
不同的网络请求
这里不同情况的封装列举例子进行讲解
请求地址中包含变量
比如请求BaseUrl+ "m/uservehicle/preferenced/1815141515"
这里最后的一个地址参数是可变的,封装入下
public interface SetUserVehiclePerferencedService {
@GET("m/uservehicle/preferenced/{vehicleId}")
Observable<HttpReply<Boolean>> setVehiclePerferenced(@Path("vehicleId") String vehicleId);
}
将变量利用{}封装,使用@path注解参数
请求地址中包含查询变量
还有一种比较常见的请求地址BaseUrl + “common/content/getarticlebyid?articleID =xxxxx”
articleId其实是查询参数,封装如下:
public interface GetNewsInfoByIdService {
@GET("common/content/getarticlebyid")
Observable<HttpReply<NewsInfoData>> getNewsInfo(@Query("articleID") String articleId);
}
@GET("group/{id}/users")
Observable<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
很简单,使用@Query作为注解,@QueryMap传入多个参数
上传Json/上传实体
如果请求中需要我们上传实体反序列化的Json字符串,上传对象的,
封装如下:
public interface UpdateUserInfoService {
@POST("m/appuser/updat")
Observable<HttpReply<Boolean>> updateUserInfo(@Body UserInfo userInfo);
}
同样很简单,使用@Body注解参数即可。
Form提交,提交参数
当你需要进行表单提交提交参数的时候
封装如下:
@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
使用@Field注解添加表单提交参数,@FieldMap添加多个参数
Tips:使用@Filed时,需要添加@FormUrlEncoded注解来进行编码
上传单个文件
/**
* 更换用户图像
*/
public interface UploadUserAvatarService {
@Multipart
@POST("m/appuser/upload/{userId}")
Observable<HttpReply<Boolean>> upload(@Path("userId") String id, @Part MultipartBody.Part avatar);
}
这里上传文件的话,必须将上传的文件构建成MultipartBody.Part的实体(因为Retrofit2.0实际上默认使用OkHttp作为HttpClient,所以MultipartBody.Part 实际上来自OkHttpClent),然后利用@Part来注解MultipartBody.Part ,下面向大家展示如何生成MultipartBody.Part
/**
* 上传用户头像
*
* @param subscriber
* @param accessToken
* @param avatar
* @param userId
*/
public void uploadAvatar(Subscriber<HttpReply<Boolean>> subscriber, String accessToken, File avatar, String userId) {
MultipartBody.Part avatarP = MultipartBody.Part.createFormData("file", "avatar.jpg", RequestBody.create(MediaType.parse("image/jpeg"), avatar));
createFileService(TrafficHttpContrains.UploadUserAvatarService.class, accessToken).upload(userId, avatarP)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
}
如上所示,RequestBody.create创建File的RequestBody(来自okHttp),第一个参数用来设置文件类型,第二个参数为上传文件
利用MultipartBody.Part.createFormData函数再次封装RequestBody作为Retrofit请求参数,第一个参数”file“表示提交给后台的参数,相当与前文中@Field(”file“)就行修饰;
第二个参数就是你上传文件自定义的文件名了。
综上,文件,文件名,文件类型,对应参数,这么就直接构成了单个文件上传。
多表单提交(参数和文件混合)
使用MutilForm,多表单提交时,当我想要提交多个文件的时候,同时还要上传多个参数的时候,也就是多表单提交既包括关键字,又包括文件的时候,怎么办?
这里其实是困扰了我许久的地方,解救方案就是将所有参数封装成RequestBody,接口封装如下:
/**
* 上传证据
*/
public interface UploadProofsService {
@Multipart
@POST("m/accident/upload")
Observable<HttpReply<Boolean>> uploadProofs(@Part List<MultipartBody.Part> fileMap, @PartMap HashMap<String, RequestBody> map);
}
以@Part作为注解所有的文件都封装在一个MultipartBody.Part的List中,以@PartMap作为注解所有的其他参数封装到关键字–>RequestBody的实体中。(这里其实都源于okHttp的使用)
如何将参数进行封装,实现如下:
/**
* 证据上传
*/
public void upLoadProofs(Subscriber<HttpReply<Boolean>> subscriber, String accessToken, String accidentId, HashMap<Integer, String> photoMap, List<ProofTemplate> proofTemplateList, File recorderFile, String accidentType, String timestamp, String vehicleCount, String position, String roadName, String regionCode, String regionName, String longitude, String latitude, String weather, String accidentSource) {
//添加FileMap
List<MultipartBody.Part> list = new ArrayList<>();
//添加图片证据
for (Map.Entry<Integer, String> entry : photoMap.entrySet()) {
MultipartBody.Part proof = MultipartBody.Part.createFormData("file", proofTemplateList.get(entry.getKey()).getName() + ".jpg", RequestBody.create(MediaType.parse("image/jpeg"), new File(entry.getValue())));
list.add(proof);
}
//添加录音证据
if (null != recorderFile && recorderFile.exists()) {
MultipartBody.Part record = MultipartBody.Part.createFormData("file", AccidentProof.ProofTypeEn[AccidentProof.ProofTypeEn.length - 1] + ".amr", RequestBody.create(MediaType.parse("application/octet-stream"), recorderFile));
list.add(record);
}
//添加其他的参数Body
RequestBody accidentTypeRb = RequestBody.create(MediaType.parse("text/plain"), accidentType);
RequestBody timeStampRb = RequestBody.create(MediaType.parse("text/plain"), timestamp);
RequestBody vehicleCountRb = RequestBody.create(MediaType.parse("text/plain"), vehicleCount);
RequestBody positionRb = RequestBody.create(MediaType.parse("text/plain"), position);
RequestBody roadNameRb = RequestBody.create(MediaType.parse("text/plain"), roadName);
RequestBody regionCodeRb = RequestBody.create(MediaType.parse("text/plain"), regionCode);
RequestBody regionNameRb = RequestBody.create(MediaType.parse("text/plain"), regionName);
RequestBody longitudeRb = RequestBody.create(MediaType.parse("text/plain"), longitude);
RequestBody latitudeRb = RequestBody.create(MediaType.parse("text/plain"), latitude);
RequestBody weatherRb = RequestBody.create(MediaType.parse("text/plain"), weather);
RequestBody accidentSourceRb = RequestBody.create(MediaType.parse("text/plain"), accidentSource);
RequestBody accidentIdRb = RequestBody.create(MediaType.parse("text/plain"), accidentId);
//添加FileMap
HashMap<String, RequestBody> map = new HashMap<>();
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_ACCIDENT_TYPE, accidentTypeRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_TIME_STAMP, timeStampRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_COUNT, vehicleCountRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_POSITION, positionRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_ROAD_NAME, roadNameRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_REGION_CODE, regionCodeRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_REGION_NAME, regionNameRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_LONGITUDE, longitudeRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_LATITUDE, latitudeRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_WEATHER, weatherRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_VEHICLE_ACCIDENT_SOURCE, accidentSourceRb);
map.put(HttpRequstConstant.UPLOAD_PROOF_PARA_ID, accidentIdRb);
//默认给请求头添加了contentType json
createFileService(TrafficHttpContrains.UploadProofsService.class, accessToken).uploadProofs(list, map)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
}
如上,将图片文件以及录音文件,封装成MultipartBody.Part,封装方式和上传单个文件一样,然后添加到List当中,值得注意的地方是图片的contentType应该为”image/jpeg”,而录音以及其他文件使用流传输,可以将contentType设置为”application/octet-stream”。
对于其他参数的设置就是封装成RequestBody,很简单不用多说。
值得一提的是使用多表单上传文件的时候,需要把请求时候的请求头中”Content-Type”设置为
“multipart/form-data”,这个在下一节中会讲到。
Http请求Header的设置
这里的大部分例子来源于Retrofit官网。
固定Header设置
使用@Headers注解,如下:
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
@Headers({
"Accept: application/vnd.github.v3.full+json",
"User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);
Header中带有变量
使用@Header注解作为参数,如下:
@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
使用OkHttp interceptor添加Header
当你有大量的请求需要添加Header时,这样在每个接口中书写一遍Header是一件很麻烦的事情,最常见的情况就是Oauther授权,AccessToken访问。这里处理方式就是在使用Retrofit执行接口的时候回创建OkHttpClient,给OkHttpClient添加Header配置。
实现如下:
// 获取用户信息等一系列需要用到AccessToken的Http请求的头的设置
public static final String STANDARD_HTTP_HEAD_AUTHORIZATION_KEY = "Authorization";
public static final String STANDARD_HTTP_HEAD_AUTHORIZATION_VALUE = "Bearer ";
public static final String STANDARD_HTTP_HEAD_CONTENT_TYPE_KEY = "Content-Type";
public static final String STANDARD_HTTP_HEAD_CONTENT_TYPE_VALUE = "application/json;charset=UTF-8";
public static final String STANDARD_HTTP_HEAD_ACCESS_TOKEN_KEY = "x-auth-token";
/**
* 正常的包括AccessToken请求头的请求,Json
*
* @param serviceClass
* @param accessToken
* @param <S>
* @return
*/
public <S> S createService(Class<S> serviceClass, final String accessToken) {
if (accessToken != null) {
//添加固定的请求Header
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_AUTHORIZATION_KEY, HttpRequstConstant.STANDARD_HTTP_HEAD_AUTHORIZATION_VALUE + accessToken)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_CONTENT_TYPE_KEY, HttpRequstConstant.STANDARD_HTTP_HEAD_CONTENT_TYPE_VALUE)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_ACCESS_TOKEN_KEY, accessToken)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_DEVICE_TOKEN_KEY, SecurityApplication.device_token)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_DEVICE_TYPE_KEY, SecurityApplication.device_type)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_VERSION_KEY, SecurityApplication.VERSION)
.method(original.method(), original.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
}
//创建Retrofit客户端
OkHttpClient client = httpClient.build();
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}
如上,每次调用接口的时候使用CreatService(clz,accessToken)就可以了,
例子中包括了AccessToken信息,以及服务器端自定义的需要的device,version等请求头信息。
最后附上多表单提交File时候,请求头的设置,你需要请求头中”Content-Type”设置为
“multipart/form-data”
public static final String MULTIPART_FORM_DATA = "multipart/form-data";
/**
* 正常的包括AccessToken请求头的请求,multipart内容类型
*
* @param serviceClass
* @param accessToken
* @param <S>
* @return
*/
public <S> S createFileService(Class<S> serviceClass, final String accessToken) {
if (accessToken != null) {
//添加固定的请求Header
httpClient.addInterceptor(new Interceptor() {
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request original = chain.request();
// Request customization: add request headers
Request.Builder requestBuilder = original.newBuilder()
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_AUTHORIZATION_KEY, HttpRequstConstant.STANDARD_HTTP_HEAD_AUTHORIZATION_VALUE + accessToken)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_CONTENT_TYPE_KEY, MULTIPART_FORM_DATA)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_ACCESS_TOKEN_KEY, accessToken)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_DEVICE_TOKEN_KEY, SecurityApplication.device_token)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_DEVICE_TYPE_KEY, SecurityApplication.device_type)
.header(HttpRequstConstant.STANDARD_HTTP_HEAD_VERSION_KEY, SecurityApplication.VERSION)
.method(original.method(), original.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
});
}
//创建Retrofit客户端
OkHttpClient client = httpClient.build();
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}
文件下载
下载固定地址下的文件,接口如下:
/**
* 下载文件
*/
public interface DownLoadFileService {
@GET
Observable<ResponseBody> downLoad(@Url String fileUrl);
}
/**
* 下载文件(大型文件)
*/
public interface DownLoadFileService {
@Streaming
@GET
Observable<ResponseBody> downLoad(@Url String fileUrl);
}
大型文件添加@Streaming注解,在返回的回调中从ResponseBody中取出数据,然后储存成文件,储存代码如下:
/**
* 存储retrofit下载的文件
*
* @param body
* @param filePath
* @return
*/
public boolean writeResponseBodyToDisk(ResponseBody body, String filePath) {
LogUtil.e("securities", "writeResponseBodyToDisk body" + body.contentLength());
try {
File futureStudioIconFile = new File(filePath);
InputStream inputStream = null;
OutputStream outputStream = null;
try {
byte[] fileReader = new byte[4096];
long fileSize = body.contentLength();
long fileSizeDownloaded = 0;
inputStream = body.byteStream();
outputStream = new FileOutputStream(futureStudioIconFile);
while (true) {
LogUtil.e("", "file read before inputStream available =" + inputStream.available());
int read = inputStream.read(fileReader);
LogUtil.e("", "file read after ");
if (read == -1) {
LogUtil.e("", "file read == -1 ");
break;
}
outputStream.write(fileReader, 0, read);
LogUtil.e("", "file write after ");
fileSizeDownloaded += read;
LogUtil.e("", "file download: " + fileSizeDownloaded + " of " + fileSize);
}
outputStream.flush();
return true;
} catch (IOException e) {
LogUtil.e("securities", "writeResponseBodyToDisk fail e" + e.toString());
return false;
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
} catch (IOException e) {
LogUtil.e("securities", "writeResponseBodyToDisk fail e" + e.toString());
return false;
}
}
PS:最近貌似有人试过,上传文件不用设置请求头中的content-type为”multipart/form-data”,待验证,后续有时间,会就Rxjava在进行详细的讲解