Android 手把手教你使用Retrofit2
前言:
1.我想通过本文,让大家能更快的上手Retrofit2,跟上时代的步伐。
2.本文主要是写给初级、中级开发人员阅读,所以在这边不会做太多的解释说明,只针对一些坑以及使用方法进行说明。
3.本文只涉及到Retrofit2,并未与RxJava一起使用,RxJava+Retrofit2技术将会在下篇文章中呈现。
4.本人也是刚开始接触Retrofit2,所以可能文章中会有一些错误或者不足,希望大家多多留言探讨,共同进步
介绍个框架给大家:
Novate https://github.com/Tamicer/Novate
感兴趣的朋友可以用用,还不错,相关资料在
Novate:Retrofit2.0和RxJava的又一次完美改进加强!(九)
大家也可以关注他:@Tamic,里面的文章都还不错,至少排版上比我的好多了,哈哈
开启Retrofit2之旅
添加依赖
以下是我用到的一些依赖
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'
//日志拦截器
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
compile 'io.reactivex:rxjava:1.2.4'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'org.ligboy.retrofit2:converter-fastjson-android:2.1.0'
注解
retrofit通过使用注解来简化请求,大体分为以下几类:
1.用于标注请求方式的注解
2.用于标记请求头的注解
3.用于标记请求参数的注解
4.用于标记请求和响应格式的注解
请求方法注解
注解 | 说明 |
---|---|
@GET | get请求 |
@POST | post请求 |
@PUT | put请求 |
@DELETE | delete请求 |
@PATCH | patch请求,该请求是对put请求的补充,用于更新局部资源 |
@HEAD | head请求 |
@OPTIONS | option请求 |
@HTTP | 通用注解,可以替换以上所有的注解,其拥有三个属性:method,path,hasBody |
请求头注解
注解 | 说明 |
---|---|
@Headers | 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在 |
@Header | 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头 |
请求参数注解
名称 | 说明 |
---|---|
@Body | 多用于post请求发送非表单数据,比如想要以post方式传递json格式数据 |
@Filed | 多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用 |
@FiledMap | 和@Filed作用一致,用于不确定表单参数 |
@Part | 用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况 |
@PartMap | 用于表单字段,默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传 |
@Path | 用于url中的占位符 |
@Query | 用于Get中指定参数 |
@QueryMap | 和Query使用类似 |
@Url | 指定请求路径 |
请求和响应格式注解
名称 | 说明 |
---|---|
@FormUrlEncoded | 表示请求发送编码表单数据,每个键值对需要使用@Field注解 |
@Multipart | 表示请求发送multipart数据,需要配合使用@Part |
@Streaming | 表示响应用字节流的形式返回.如果没使用该注解,默认会把数据全部载入到内存中.该注解在在下载大文件的特别有用 |
使用
这里不对注解进行过多的说明了,只展示几个用法
/**
* Created by caihan on 2017/1/11.
*
* @GET 表明这是get请求
* @POST 表明这是post请求
* @PUT 表明这是put请求
* @DELETE 表明这是delete请求
* @PATCH 表明这是一个patch请求,该请求是对put请求的补充,用于更新局部资源
* @HEAD 表明这是一个head请求
* @OPTIONS 表明这是一个option请求
* @HTTP 通用注解, 可以替换以上所有的注解,其拥有三个属性:method,path,hasBody
* @Headers 用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在
* @Header 作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头
* @Body 多用于post请求发送非表单数据, 比如想要以post方式传递json格式数据
* @Filed 多用于post请求中表单字段, Filed和FieldMap需要FormUrlEncoded结合使用
* @FiledMap 和@Filed作用一致,用于不确定表单参数
* @Part 用于表单字段, Part和PartMap与Multipart注解结合使用, 适合文件上传的情况
* @PartMap 用于表单字段, 默认接受的类型是Map<String,REquestBody>,可用于实现多文件上传
* <p>
* Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。
* 而Filed则相对简单些,通常是字符串键值对。
* </p>
* Part标志上文的内容可以是富媒体形势,比如上传一张图片,上传一段音乐,即它多用于字节流传输。
* 而Filed则相对简单些,通常是字符串键值对。
* @Path 用于url中的占位符,{占位符}和PATH只用在URL的path部分,url中的参数使用Query和QueryMap代替,保证接口定义的简洁
* @Query 用于Get中指定参数
* @QueryMap 和Query使用类似
* @Url 指定请求路径
*/
public interface MyRetrofit2Service {
//使用@Headers添加多个请求头
@Headers({
"User-Agent:android",
"apikey:123456789",
})
@POST()
Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);
@GET("mobile/active")
Call<HttpResult<News>> get(@Header("token") String token, @Query("id") int activeId);
@GET("active/list")
Call<HttpResult<News>> ActiveList();
@POST("api/news/newsList")
Call<HttpResult<News>> post2(@QueryMap Map<String, String> map);
/**
* 很多情况下,我们需要上传json格式的数据。比如当我们注册新用户的时候,因为用户注册时的数据相对较多,
* 并可能以后会变化,这时候,服务端可能要求我们上传json格式的数据。此时就要@Body注解来实现。
* 直接传入实体,它会自行转化成Json
*
* @param url
* @param post
* @return
*/
@POST("api/{url}/newsList")
Call<HttpResult<News>> login(@Path("url") String url, @Body News post);
/**
* 单张图片上传
* retrofit 2.0的上传和以前略有不同,需要借助@Multipart注解、@Part和MultipartBody实现。
*
* @param url
* @param file
* @return
*/
@Multipart
@POST("{url}")
Call<HttpResult<News>> upload(@Path("url") String url, @Part MultipartBody.Part file);
/**
* 多张图片上传
*
* @param map
* @return
*/
@Multipart
@POST("upload/upload")
Call<HttpResult<News>> upload(@PartMap Map<String, MultipartBody.Part> map);
/**
* 图文混传
*
* @param post
* @param map
* @return
*/
@Multipart
@POST("")
Call<HttpResult<News>> register(@Body News post, @PartMap Map<String, MultipartBody.Part> map);
/**
* 文件下载
*
* @param fileUrl
* @return
*/
@Streaming
@GET
Call<HttpResult<News>> downloadPicture(@Url String fileUrl);
/**
* 这里需要注意的是如果下载的文件较大,比如在10m以上,那么强烈建议你使用@Streaming进行注解,否则将会出现IO异常.
*
* @param fileUrl
* @return
*/
@Streaming
@GET
Observable<HttpResult<News>> downloadPicture2(@Url String fileUrl);
@POST()
@FormUrlEncoded
Observable<HttpResult<News>> executePost(@FieldMap Map<String, Object> maps);
}
踩坑
1.url被转义
http://api.mydemo.com/api%2Fnews%2FnewsList?
罪魁祸首@Url与@Path注解,我们开发过程中,肯定会需要动态的修改请求地址
两种动态修改方式如下:
@POST()
Call<HttpResult<News>> post(@Url String url, @QueryMap Map<String, String> map);
@POST("api/{url}/newsList")
Call<HttpResult<News>> login(@Path("url") String url, @Body News post);
第一种是直接使用@Url,它相当于直接替换了@POST()里面的请求地址
第二种是使用@Path(“url”),它只替换了@POST(“api/{url}/newsList”)中的{url}
如果你用下面这样写的话,就会出现url被转义
@POST("{url}")
Call<HttpResult<News>> post(@Path("url") String url);
你如果执意要用@Path,也不是不可以,需要这样写
@POST("{url}")
Call<HttpResult<News>> post(@Path(value = "url", encoded = true) String url);
OkHttpClient
拦截器
addNetworkInterceptor添加的是网络拦截器Network Interfacetor它会在request和response时分别被调用一次;
addInterceptor添加的是应用拦截器Application Interceptor他只会在response被调用一次。
1.日志拦截器
使用addNetworkInterceptor()
方法添加到OkHttpClient中
日志拦截器我这有两种创建方式:
一种是使用HttpLoggingInterceptor,需要使用到依赖
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
/**
* 创建日志拦截器
*
* @return
*/
public static HttpLoggingInterceptor getHttpLoggingInterceptor() {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(
new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.e("OkHttp", "log = " + message);
}
});
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return loggingInterceptor;
}
另一种是
/**
* 创建日志拦截器
*
* @return
*/
private class LogInterceptor implements Interceptor {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
Log.d("OkHttp", "HttpHelper1" + String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
okhttp3.Response response = chain.proceed(request);
long t2 = System.nanoTime();
Log.d("OkHttp", "HttpHelper2" + String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
2.请求头拦截器
使用addInterceptor()
方法添加到OkHttpClient中
我的理解是,请求头拦截器是为了让服务端能更好的识别该请求,服务器那边通过请求头判断该请求是否为有效请求等…
/**
* 拦截器Interceptors
* 使用addHeader()不会覆盖之前设置的header,若使用header()则会覆盖之前的header
*
* @return
*/
public static Interceptor getRequestHeader() {
Interceptor headerInterceptor = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
builder.addHeader("version", "1");
builder.addHeader("time", System.currentTimeMillis() + "");
Request.Builder requestBuilder = builder.method(originalRequest.method(), originalRequest.body());
Request request = requestBuilder.build();
return chain.proceed(request);
}
};
return headerInterceptor;
}
3.统一请求拦截器
使用addInterceptor()
方法添加到OkHttpClient中
统一请求拦截器的功能跟请求头拦截器相类似
/**
* 拦截器Interceptors
* 统一的请求参数
*/
private Interceptor commonParamsInterceptor() {
Interceptor commonParams = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request originRequest = chain.request();
Request request;
HttpUrl httpUrl = originRequest.url().newBuilder().
addQueryParameter("paltform", "android").
addQueryParameter("version", "1.0.0").build();
request = originRequest.newBuilder().url(httpUrl).build();
return chain.proceed(request);
}
};
return commonParams;
}
4.时间拦截器
使用addInterceptor()
方法添加到OkHttpClient中
从响应中获取服务器返回的时间
/**
* 拦截器Interceptors
* 通过响应拦截器实现了从响应中获取服务器返回的time
*
* @return
*/
public static Interceptor getResponseHeader() {
Interceptor interceptor = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response response = chain.proceed(chain.request());
String timestamp = response.header("time");
if (timestamp != null) {
//获取到响应header中的time
}
return response;
}
};
return interceptor;
}
缓存
使用okhttp缓存的话,先要创建Cache,然后在创建缓存拦截器
//创建Cache
File httpCacheDirectory = new File(MyApp.getContext().getCacheDir(), "OkHttpCache");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
httpClientBuilder.cache(cache);
//设置缓存
httpClientBuilder.addNetworkInterceptor(getCacheInterceptor2());
httpClientBuilder.addInterceptor(getCacheInterceptor2());
缓存拦截器
缓存时间自己根据情况设定
/**
* 在无网络的情况下读取缓存,有网络的情况下根据缓存的过期时间重新请求,
*
* @return
*/
public Interceptor getCacheInterceptor2() {
Interceptor commonParams = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!NetworkUtils.isConnected()) {
//无网络下强制使用缓存,无论缓存是否过期,此时该请求实际上不会被发送出去。
request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE)
.build();
}
okhttp3.Response response = chain.proceed(request);
if (NetworkUtils.isConnected()) {//有网络情况下,根据请求接口的设置,配置缓存。
//这样在下次请求时,根据缓存决定是否真正发出请求。
String cacheControl = request.cacheControl().toString();
//当然如果你想在有网络的情况下都直接走网络,那么只需要
//将其超时时间这是为0即可:String cacheControl="Cache-Control:public,max-age=0"
int maxAge = 60 * 60; // read from cache for 1 minute
return response.newBuilder()
// .header("Cache-Control", cacheControl)
.header("Cache-Control", "public, max-age=" + maxAge)
.removeHeader("Pragma")
.build();
} else {
//无网络
int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
return response.newBuilder()
// .header("Cache-Control", "public,only-if-cached,max-stale=360000")
.header("Cache-Control", "public,only-if-cached,max-stale=" + maxStale)
.removeHeader("Pragma")
.build();
}
}
};
return commonParams;
}
自定义CookieJar
httpClientBuilder.cookieJar(new CookieJar() {
final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url, cookies);//保存cookie
//也可以使用SP保存
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url);//取出cookie
return cookies != null ? cookies : new ArrayList<Cookie>();
}
});
启动Retrofit2
到了这里,基本上准备工作都做好了,可以启动Retrofit2了
retrofit = new Retrofit.Builder()
.client(httpClientBuilder.build())
.baseUrl(BASE_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(FastJsonConverterFactory.create())
.build();
mApi = retrofit.create(MyRetrofit2Service.class);
界面上通过getEnqueue()方法调用
/**
* 异步调用
*/
public void getEnqueue() {
Call<HttpResult<News>> call = mApi.post(NEWS_URI, params);
call.enqueue(new Callback<HttpResult<News>>() {
@Override
public void onResponse(Call<HttpResult<News>> call, Response<HttpResult<News>> response) {
//处理请求成功
Log.e("OkHttp", "处理成功请求 response = " + response.body().toString());
}
@Override
public void onFailure(Call<HttpResult<News>> call, Throwable t) {
//处理请求失败
Log.e("OkHttp", "处理失败请求");
}
});
// cancelCall(call);
}
结束
相信到这边应该能满足Demo的要求了吧,至少用是可以用了,不过在实际开发中,这还是太糙了点,哎..没时间打磨,慢慢来吧,有兴趣的可以留言讨论下优化方案,大家拓展下思维也是不错的嘛