一. 为什么写这篇文章
RxJava和Retrofit也火了一段时间了,网上有很多RxJava与Retrofit的文章介绍,拜读一番之后,仍感觉这两个框架不接底气,对于初学者仍是丈二的和尚,不知所云,且知识点比较零碎,故写下这篇文章。
目的一: 把知识点通过一篇文章涵盖的尽量全面,减少学习成本
目的二:结合实际开发自己封装一个比较通用网络请求
目的三:从理解的层面出发,把踩过的坑明确说明,减少使用疑惑
二. 这篇文章都讲了什么
大白话叙述什么是Retrofit2
Retrofit2的基本知识和用法
Retrofit2自定义拦截器,配置全局请求头
Retrofit2自定义解析器,数据传递加解密
Rxjava是干什么的,替代了那些好理解的代码
Rxjava基本知识和用法
Rxjava结合Retrofit2的使用
Rxjava结合Retrofit2实际开发中的封装和使用
三.说明
因本人水平有限,这篇文章只是一个入门级的,如有错误,欢迎骚扰 qq:807142217
四.进入正题:
一.什么是Retrofit2?
一句话:包装OKhttp联网框架的一个框架。其真正去网络请求的还是okHttp,所以大家也都说它是一个联网框架。okhttp设置请求方式,BaseUrl,设置请求头,设置请求参数等想必大家都已经了解,而Retrofit2做的只是通过 注解、interface和动态代理等技术手段改变了设置请求的方式。换句话来说,我们就是在学习Retrofit定义设置的方式,也即是所谓的注解所代表的含义和普通设置OKHttp设置网络请求画等号的过程。
那么问题就来了:只是改变了设置请求的方式,为什么会有这么多人使用,而且会这么火?正是因为他包装的网络请求返回结果支持响应式编程,也就是Rxjava,所以它火,就我个人理解而言,如果不是因为他支持Rxjava,我想大家也不会闲着没事去封装一个这样无聊的框架。既然它是因为Rxjava,那么什么又是Rxjava呢?
二. 什么是Rxjava?
也就是响应式编程,那么什么又是响应式编程呢? 就是:异步。就是替代Android原生的Handler来实现线程切换和数据传递的杂乱处境。众所周知,网络请求要在子线程中进行,而更新UI要在Main线程中,因此Google发明了Handler,试想一个Activity进行多次线程之间的切换,都要使用Handler,代码是多零散,即使你用Handler实现了需求,待你再过几个月去维护这些代码的时候,我想你自己都不知道写的是什么,而响应式编程,正是一行代码解决线程切换和线程之间等待的问题,所以我们为什么要学习Retrofit2和Rxjava。
三. Retrofit2的基本使用(也就是:注解所代表的含义和普通设置OKHttp设置网络请求画等号的过程。)
看到这里,如果你还不知道OKHttp怎么使用,那就去看一下[鸿洋大神](http://blog.csdn.net/lmj623565791/article/details/47911083) ,再回过头来看这篇文章:
初识框架
需求:
使用get请求需要传递四个参数口:
https://api.douban.com/v2/ book/search ?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&tag=&start=0&count=3
public interface BlueService {
@GET("book/search")
Call getSearchBooks(@Query("q") String name,
@Query("tag") String tag, @Query("start") int start,
@Query("count") int count);//BookSearchResponse是一个返回的Bean对象,实际类要根据接口返回对象去添加设置
}
说明:
@GET注解就表示get请求,"()"里边的字符串表示变动的url部分
@Query表示请求参数,"()"里边的字符串表示请求的key部分内容
@Query只能用于Get请求中
通过以上步骤发现只传递过去部分url和key, 剩余的BaseUrl和value怎么传递过去呢?这就要靠第二步和第四步
2.在Activity页面创建一个Retrofit的实例,传递BseeUrl还有其他配置,下文会讲到,如:GsonConverterFactory.create()表示调用自带解析器来解析json返回值; .addCallAdapterFactory(RxJavaCallAdapterFactory.create())表示结合Rxjava使用的Adapter,如不结合,不必添加
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.douban.com/v2/")//要以/结束否者会报错
.addConverterFactory(GsonConverterFactory.create())//解析的对象
.build();
经过以上两步Retrofit框架会自动拼接一个相对完整的url,这个时候你会发现只差value没有设置了,这一步等你看到第四步自然会明白
3:通过Retrofit的实例的create()方法,传入接口生成对应的服务类
BlueService service = retrofit.create(BlueService.class);
4:调用请求方法,并得到Call实例
Call call = mBlueService.getSearchBooks("小王子", "", 0, 3); //注意这里传递的value,且第二个参数这里填写的是空字符串
这个时候有的同学就会纳闷了,明明自己就写了一个接口,怎么就可以去调用了呢?这里就不得不提一下动态代理这个词了
其实内部是通过反射和解析,去生成一个实现类,而不用我们去考虑具体是怎么实现了,这就是框架的好处
5:使用Call实例完成同步或异步请求
5.1 同步请求(注意不能在主线程调用)
BookSearchResponse response = call.execute().body();
5.2 异步请求(回调在主线程中)
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
//这里是Main线程,即使解析失败,还是会回调这个方法,详细后边在做解释
}
@Override
public void onFailure(Call call, Throwable t) {
}
});
如何使用
compile 'com.squareup.retrofit2:retrofit:2.1.0'//这是联网框架的依赖
compile 'com.squareup.retrofit2:converter-gson:2.1.0'//这是解析的依赖
注意:
如果要结合Rxjava使用需要添加下边依赖,且rxjava的依赖版本号必须和retrofit的版本号一样,否者会报错
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
接下来我们将在围绕注解来介绍使用规则,毕竟请求方式以及参数都是通过接口传递过去的,实体方法还是框架自动生成的
Get请求方式有关的注解,以及数据传递规则和拼接url的方式
首先要十分明确:Get方法请求参数都会以key=value的方式拼接在url后面
get请求参数传递方式:Retrofit提供了两种方式设置请求参数
1:就是像上文提到的直接在interface中添加@Query注解,不再做叙述
2:通过Interceptor(拦截器)实现请求参数的添加,一般用在设置全局的参数
2.1 实现 Interceptor接口
2.2.addQueryParameter就是添加请求参数的具体代码
public class CustomInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//2.获取请求对象
Request request = chain.request();
//3.获取封装请求参数的对象,去添加参数
HttpUrl build = request.url().newBuilder().addQueryParameter("token", "tokenValue").build();
//4.把封装过请求数据的对象添加在请求对象里边
Request build1 = request.newBuilder().url(build).build();
return chain.proceed(build1);
}
}
2.3创建完成自定义的Interceptor后,还需要在Retrofit创建client处完成添加
OkHttpClient.Builder client = new OkHttpClient.Builder()
.addInterceptor(new CustomInterceptor())//添加请求拦截
.retryOnConnectionFailure(true);
2.4 添加自己设置过拦截器的OKhttpClient对象,上文已经说过,是包装的OKhttp
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.douban.com/v2/") //要以结束否者会报错
.addConverterFactory(GsonConverterFactory.create())//解析的对象
.client(client.build())//添加自己设置过拦截器的OKhttpClient对象
.build();
因为注解比较多,接下来我将把常用到的注解给大家快速归类一下,如果想整整理解还需要自己尝试,在这里推荐一篇文章Retrofit注解
@QueryMap
如果Query参数比较多,那么可以通过@QueryMap方式将所有的参数集成在一个Map统一传递,还以上文中的get请求方法为例
public interface BlueService {
@GET("book/search")
Call getSearchBooks(@QueryMap Map options);
}
调用的时候将所有的参数集合在统一的map中即可
Map<String, String> options = new HashMap<>();
map.put("q", "小王子");
map.put("tag", null);
map.put("start", "0");
map.put("count", "3");
Call call = mBlueService.getSearchBooks(options);
Query非必填
如果请求参数为非必填,也就是说即使不传该参数,服务端也可以正常解析,那么如何实现呢?其实也很简单,请求方法定义处还是需要完整的Query注解,某次请求如果不需要传该参数的话,只需填充null即可。
针对文章开头提到的get的请求,加入按以下方式调用
Call 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
@Path
如果请求的相对地址也是需要调用方传递,那么可以使用@Path注解,示例代码如下:
@GET("book/{id}")
Call getBook(@Path("id") String id) ;
业务方想要在地址后面拼接书籍id,那么通过Path注解可以在具体的调用场景中动态传递,具体的调用方式如下:
Call call = mBlueService.getBook("1003078");
此时的url地址为
https://api.douban.com/v2/book/1003078
Post请求
@field
Post请求需要把请求参数放置在请求体中,而非拼接在url后面,先来看一个简单的例子
@FormUrlEncoded //在post请求中这个注解必须填写,否者会报错
@POST("book/reviews")
Call addReviews(@Field("book") String bookId, @Field("title") String title,
@Field("content") String content, @Field("rating") String rating);//必须有@Field注解,也就比必须有参数,否者会报错
注意:
@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将会被编码,即将中文和特殊字符进行编码转换
@FieldMap
上述Post请求有4个请求参数,假如说有更多的请求参数,那么通过一个一个的参数传递就显得很麻烦而且容易出错,这个时候就可以用FieldMap
@FormUrlEncoded
@POST("book/reviews")
Call addReviews(@FieldMap Map fields) ;
@Body
如果Post请求参数有多个,那么统一封装到类中应该会更好,这样维护起来会非常方便
@FormUrlEncoded
@POST("book/reviews")
Call addReviews(@Body Reviews reviews) ;
public class Reviews {
public String book;
public String title;
public String content;
public String rating;
}
这时,内部会转化成json字符串,然后提交给服务器
关于注解的更多知识,请查看上文给出的链接地址
不得不说的知识
网络请求日志
调试网络请求的时候经常需要关注一下请求参数和返回值,请求头和响应头,以便判断和定位问题出在哪里,但又不会抓包工具,Retrofit官方提供了一个很方便查看日志的Interceptor,方便开发者查看
首先需要在build.gradle文件中引入logging-interceptor
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
其次,想上文添加自定义拦截器的方式一样把log拦截器添加到请求中
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
添加请求头,或者配置全局的请求头
1.添加请求头
静态方法
public interface BlueService {
@Headers("Cache-Control: max-age=640000")
@GET("book/search")
Call 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 getSearchBooks(@Query("q") String name,
@Query("tag") String tag, @Query("start") int start,
@Query("count") int count);
}
动态方法
public interface BlueService {
@GET("book/search")
Call getSearchBooks(
@Header("Content-Range") String contentRange,
@Query("q") String name, @Query("tag") String tag,
@Query("start") int start, @Query("count") int count);
}
配置全局的请求头
public class RequestInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("User-Agent", "Your-App-Name")
.addHeader("key","value")
.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();
}
那么问题来了,通过注解也可以设置请求头,通过拦截器也可以设置请求头,那么谁会覆盖谁呢? 通过打印请求头得到结果:拦截器有可能会覆盖注解设置的请求头,关键 看拦截器调用的是header(key, value)还是addHeader(key, value) ,如果是header就会覆盖,
通过以上效果可以根据需求来动态设置请求头
但是如果您的需求要求数据传递都需要加密解密的情况下,那么默认的解析器显然不能瞒住要求
没有什么好说的,直接上代码
public final class DecodeConverterFactory extends Converter.Factory {
public static DecodeConverterFactory create() {
return create(new Gson());
}
public static DecodeConverterFactory create(Gson gson) {
return new DecodeConverterFactory(gson);
}
private final Gson gson;
private DecodeConverterFactory(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
this.gson = gson;
}
@Override
public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter> adapter = gson.getAdapter(TypeToken.get(type));
return new DecodeResponseBodyConverter<>(gson, adapter); //响应
}
@Override
public Converter, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter> adapter = gson.getAdapter(TypeToken.get(type));
return new DecodeRequestBodyConverter<>(gson, adapter); //请求
}
}
请求
public class DecodeRequestBodyConverter<T> implements Converter<T, RequestBody> {
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
private final Gson gson;
private final TypeAdapter adapter;
/**
* 构造器
*/
public DecodeRequestBodyConverter(Gson gson, TypeAdapter adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public RequestBody convert(T value) throws IOException {
//没有做加密处理,直接传递,如果要做,就在这里边做
return RequestBody.create(MEDIA_TYPE, value.toString());
}
}
响应解密
public class DecodeResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson mGson;//gson对象
private final TypeAdapter adapter;
private AES256JNCryptor cryptor;
/**
* 构造器
*/
public DecodeResponseBodyConverter(Gson gson, TypeAdapter adapter) {
this.mGson = gson;
this.adapter = adapter;
}
/**
* 转换
*
* @param responseBody
* @return
* @throws IOException
*/
@Override
public T convert(ResponseBody responseBody) throws IOException {
String string = responseBody.string();//获取返回的json,是加密过了
if (cryptor == null) {
cryptor = new AES256JNCryptor();//解密对象
}
//解密数据,然后解析成Bean返回
try {
string = new String(cryptor.decryptData(Base64.decode(string, Base64.NO_WRAP), Pwd.pwd.toCharArray()));//解密方法
} catch (CryptorException e) {
e.printStackTrace();
}
return string==null?null:adapter.fromJson(string);//解析返回
}
}
当理解以上的每个环节,那么我们就可以封装一个单例模式的请求类
public class JkApiRequest {
private static final long DEFAULT_TIMEOUT = 5;//超时时间
private static JkApiRequest instance;//单例对象
private Retrofit retrofit;//Retrofit 对象
/**
*
* @param
* @return
* @author 私有构造方法,方法里边创建retrofit 对象
*
* @createTime 2017/4/25
* @lastModify 2017/4/25
*/
private JkApiRequest() {
retrofit = new Retrofit.Builder()
.baseUrl(Constant.BaseUrl)
.addConverterFactory(GsonConverterFactory.create())//可以添加自定义解析器和默认的解析器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加响应式编程的适配器
.client(genericClient())//添加自定义的OKHttpClient对象
.build();
}
/**
* sington
*
* @param
* @return
* @author单例模式
* @createTime 2017/4/25
* @lastModify 2017/4/25
*/
public synchronized static JkApiRequest getInstance() {
if (instance == null) {
instance = new JkApiRequest();
}
return instance;
}
//返回OkHttpClient对象
private static OkHttpClient genericClient() {
OkHttpClient httpClient = new OkHttpClient.Builder()
//添加打印级别的拦截器
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS))
.addInterceptor(new Interceptor() {//添加自定义拦截器,设置公共请求头
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
// .header("key","全局的头")//会覆盖
// .addHeader("key","全局的头")//不会覆盖
.build();
return chain.proceed(request);
}
})
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)//设置超时时间
.build();
return httpClient;
}
/**
* create api instance
*
* @param service api class
* @return
* @author
* @createTime 2017/4/25
* @lastModify 2017/4/25
*/
public T create(Class service) {
return retrofit.create(service);
}
}
Rxjava学习
内容是基于对扔物线rxjava详解的理解来编写的,目的在于自己以后用的时候更加方便快速的回忆起用法
1.Observable(被观察者) 可以通过creat创建对象,也可以通过just和from创建对象 他们是等价的
2.Subscribe (订阅)
3.Observer(观察者) 可以创建Action1 不完整的 也可以创建Observer 或者创建Subscriber。
被观察者通过订阅持有观察者的对象来调用观察者的onNext(),onCompleted() 和 onError()方法
二. 观察者模式解释:
public Button btn;
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
通过 setOnClickListener() 方法,Button 持有 OnClickListener 的引用;当用户点击时,Button 自动调用 OnClickListener 的 onClick() 方法
(Button -> 被观察者、OnClickListener -> 观察者、setOnClickListener() -> 订阅,onClick() -> 事件),就由专用的观察者模式转变成了通用的观察者模式
三. RxJava ,使用的就是通用形式的观察者模式
不同于一般的观察者模式,rxjava会把每个事件单独处理,调用N次onNext方法
当不会再有新的 onNext() 发出时,需要触发 onCompleted() 方法作为标志。
onError(): 在事件处理过程中出异常时,onError() 会被触发,同时队列自动终止,不允许再有事件发出。
onCompleted() 和 onError() 二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。
四. Observer 和 Subscriber 两个观察者的区别
1.在 RxJava 的 subscribe 过程中,Observer 也总是会先被转换成一个 Subscriber 再使用。
2.onStart(): 这是 Subscriber 增加的方法。可以用于做一些准备工作,如果对准备工作的线程有要求,他就不适合了,他总是在被订阅的线程被调用
3.unsubscribe() :用于取消订阅。在这个方法被调用后,Subscriber 将不再接收事件。 废除被观察者中持有的观察者的引用,防止内存泄漏
六. RxJava线程控制 —— Scheduler
理解重点1:
在哪个线程调用 subscribe(),就在哪个线程生产事件;
在哪个线程生产事件,就在哪个线程消费事件。
如果需要切换线程,就需要用到 Scheduler (调度器)。
七. 线程调度器
Schedulers.immediate(): 直接在当前线程运行,相当于不指定线程。这是默认的 Scheduler。
Schedulers.newThread(): 总是启用新线程,并在新线程执行操作。
Schedulers.io(): I/O 读写文件 行为模式和 newThread() 差不多,区别在于可以重用空闲的线程,比 newThread() 更有效率。可以避免创建不必要的线程。
Schedulers.computation(): 计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O 等操作限制性能的操作,
Android 还有一个专用的 AndroidSchedulers.mainThread(),它指定的操作将在 Android 主线程运行。
八 . 线程切换
subscribeOn(): 指定 被观察者创建对象和事件所发生的线程-----也叫事件的生产 包括(map针对对象的直接变换时候的线程,filter过滤时候的线程,flatMap针对整个事件队列变换时候的线程)
observeOn(): 指定 观察者对象onNext()方法被调用的线程------也叫事件的消费
九 . 多次切换线程
Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.newThread())
.map(mapOperator) // 新线程,由 observeOn() 指定
.observeOn(Schedulers.io())
.map(mapOperator2) // IO 线程,由 observeOn() 指定
.observeOn(AndroidSchedulers.mainThread)
.subscribe(subscriber); // Android 主线程,由 observeOn() 指定
高能知识点:
1.当使用了多个 subscribeOn() 的时候,只有第一个 subscribeOn() 起作用
2.当使用多个observeOn()的时候,他管理下一订阅的线程,如上边的代码所展现的
结合以上封装,使用代码
/**
* 点击事件二
*
* @param view
*/
public void Demo2(View view) {
Observable.just(5)//// IO 线程,由 subscribeOn() 指定
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())//让下边的转换在io线程
.flatMap(new Func1>() {
@Override
public Observable call(Integer integer) {
return JkApiRequest.getInstance().create(RequestServices.class)
.getTop250Observable(0, integer);//联网获取相应对象
}
})
.observeOn(AndroidSchedulers.mainThread())//让下边执行在主线程
.subscribe(new Action1() {
@Override
public void call(Bean1 bean1) {
tv.setText(bean1.title);
}
});
}