<div class="markdown_views"><p>最近看了很多关于Retrofit和Rxjava的文档介绍。终于在弄清Rxjava后顺利的弄懂了Retrofit。</p>
网上有很多人都介绍了它们的联合使用,但是我看过之后理解不是太好。可能我太笨。
不过,今天写这篇博客的目的就是想来说说它们之间如何使用以及使用的时候遇到的坑。
这两者的关系并不大,但是联合在一起使用是非常简便的。Rxjava的响应式编程加上Retrofit的注解式请求用起来是非常爽的。
并且Retrofit内置的是Okhttp,所以这更加的让Retrofit变得强大。
如果在看这篇博客的时候你对Java注解、Rxjava还有Okhttp还不够了解,建议先去了解这两个东西。
给出友情链接:
RxJava——响应式和区域化的优秀框架(java&android)
相信看到这里,你已经对上面三个知识有了了解。
那么接下来切入正题。
先来加入依赖库:
compile 'io.reactivex:rxjava:1.0.14'
compile 'io.reactivex:rxandroid:1.0.1'
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
没错,你需要的库就是这么多。若要联合它们三者使用,就必须这么多。
我来按顺序介绍。
1,Rxjava库
2,RxAndroid库
3,Retrofit适配Rxjava的库
4,Retrofit库
5,Retrofit适配Gson的库(添加了这个库后不用再添加Gson库,因为已经内置)
另外还要有Okhttp依赖库。在Android sdk中已经内置。似乎Retrofit中也内置了Okhttp,所以我项目中没有加入okhttp依赖库,但是okhttp依旧可以使用。
这里需要说明一下,第五个依赖库根据你的项目需求来添加。
一般的项目来说都是使用json数据的。若你的项目是使用xml或者其他的数据格式,那么对应的添加。
(以下版本号需要与retrofit版本号保持一致,并且以retrofit官网给出的版本号为准。)
1)Gson: compile 'com.squareup.retrofit2:converter-gson:2.0.1'
2)Jackson: compile 'com.squareup.retrofit2:converter-jackson:2.0.1'
3)Moshi: compile 'com.squareup.retrofit2:converter-moshi:2.0.1'
4)Protobuf: compile 'com.squareup.retrofit2:converter-protobuf:2.0.1' 5)Wire: compile 'com.squareup.retrofit2:converter-wire:2.0.1' 6)Simple XML: compile 'com.squareup.retrofit2:converter-simplexml:2.0.1' 7)Scalars (primitives, boxed, and String): compile 'com.squareup.retrofit2:converter-scalars:2.0.1'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
好了。依赖库加完以后。我们就开始请求了。
我们先来个测试url
private String ip = "http://gc.ditu.aliyun.com/geocoding?a=上海市&aa=松江区&aaa=车墩镇";
- 1
- 1
这个是阿里云根据地区名获取经纬度接口。
返回json数据
{
"lon":120.58531,
"level":2,
"address":"",
"cityName":"",
"alevel":4,
"lat":31.29888
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
我们先来写好实体类AliAddrsBean和IndexRequestBean
public class AliAddrsBean {
private double lon;
private int level;
private String address;
private String cityName;
private int alevel;
private double lat;
//get/set方法忽略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
public class IndexRequestBean {
private String a;//一级城市
private String aa;//二级城市
private String aaa;//三级城市
//get/set方法忽略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
Retrofit的请求管理类RetrofitManage(先写成单例)
private RetrofitManage() {
}
public static RetrofitManage getInstance() {
return RetrofitManager.retrofitManage;
}
private static class RetrofitManager {
private static final RetrofitManage retrofitManage = new RetrofitManage();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
定义一个发送网络请求的方法sendRequest()
public void sendRequest(String url) {
//每一个Call实例可以同步(call.excute())或者异步(call.enquene(CallBack<?> callBack))的被执行,
//每一个实例仅仅能够被使用一次,但是可以通过clone()函数创建一个新的可用的实例。
//默认情况下,Retrofit只能够反序列化Http体为OkHttp的ResponseBody类型
//并且只能够接受ResponseBody类型的参数作为@body
Retrofit retrofit = new Retrofit
.Builder()
.baseUrl(url)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 使用RxJava作为回调适配器
.addConverterFactory(GsonConverterFactory.create()) // 使用Gson作为数据转换器
.build();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Retrofit 实例化 注意事项:
1,每一个Call实例的用法和okhttp的call几乎一样。
2,call只能被使用一次,若再次调用会抛出异常。如果需要多次使用请使用clone
3,默认反序列化OkHttp的ResponseBody类型
4,默认只能接受ResponseBody类型的参数作为@body
5,2.0以后,get请求和post的请求的区别在call里面,注解写@get和写@post已经没有区别了
sendRequest()方法中已经写的很清楚了,不再多说。
承载一切请求的接口ApiService
这个类我要分开写,因为内容实在太多。
规则:每一个函数都必须有提供请求方式和相对URL的Http注解
Retrofit提供了5种内置的注解:GET、POST、PUT、DELETE和HEAD
注解中指定的资源是相对的URL
注解中指定的资源是相对的URL
注解中指定的资源是相对的URL
为啥说三遍,不解释。
这是第一个细节也是第一个门槛。
注解里到底写的是什么?
我们先来看看一个简单的、迷茫的get请求。
@GET("search/repositories")
Call<RetrofitBean> queryRetrofitByGetCall(
@Query("name")String name,
@Query("pwd")String pwd);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
url呢?
注解里写的啥?
query又是啥?
怎么调用?
怎么返回?
这是我在网上看到最多的示例。
我就搞不懂了,给一个初学者写这么一个东西谁看得懂?
所以,我来一步一步的解释清楚,写一个易懂的例子。
先来回顾一下我们的url:http://gc.ditu.aliyun.com/geocoding?a=上海市&aa=松江区&aaa=车墩镇
参照这个url我们来写一个get请求:
@GET("http://gc.ditu.aliyun.com/geocoding?a=上海市&aa=松江区&aaa=车墩镇")
Call<AliAddrsBean> getIndexContent();
- 1
- 2
- 1
- 2
这么看就明白了吧,注解中就是一个url而已。
看看怎么调用的
Retrofit retrofit = new Retrofit
.Builder()
.baseUrl(url) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build();
ApiService service = retrofit.create(ApiService.class);
Call<AliAddrsBean> requestInde = service.getIndexContent();
requestInde.execute();//同步请求,和okhttp的使用方法一样
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
这就是最简单粗暴的调用方法。这里需要注意的是,我们还需要传入一个实体类作为返回数据的解析依据。
这里我们就是传入的AliAddrsBean。Retrofit已经帮我们把数据解析好了。
可以看出来我们在外面调用了.baseUrl(url) ,为什么在注解中还需要写url呢?
这就关系到很多的细节和坑了。
现在我们假定我们的url是这样的:http://gc.ditu.aliyun.com/geocoding?
如果我们在注解里面这么写:
(以下有坑)
@GET("a=上海市&aa=松江区&aaa=车墩镇")
Call<AliAddrsBean> getIndexContentOne();
- 1
- 2
- 1
- 2
看似是对的,其实在我做demo的时候用okhttp请求成功但是用retrofit请求一直失败。
究其原因就是:注解中必须要有一部分的url地址,不能光是请求体。
所以,修改代码:
请求url:http://gc.ditu.aliyun.com/
以下是正解:
@GET("geocoding?a=上海市&aa=松江区&aaa=车墩镇")
Call<AliAddrsBean> getIndexContentOne();
- 1
- 2
- 1
- 2
这么一写,果然请求正常了。
另外还有一个坑,Retrofit建议url以/结束,注解不要以/开始
我们将get请求汇入接口ApiService中:
基本get请求
//实际上在get的开始已经有一个rul存在了
//例如,我们的url是“http://gc.ditu.aliyun.com/”
//那么get注解前就已经存在了这个url,并且使用{}替换符得到的最终url是:
// http://gc.ditu.aliyun.com/geocoding?a=苏州市
//参数不能为null,且不能只有url的参数,还应该包括地址的字段;正确:geocoding?a=苏州市;错误:a=苏州市
@GET("geocoding?a=苏州市")
Call<AliAddrsBean> getIndexContentOne();
@GET("http://gc.ditu.aliyun.com/geocoding?a=上海市&aa=松江区&aaa=车墩镇")
Call<AliAddrsBean> getIndexContent();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
{ }取代块和@Path
//这里需要注意的是,{}作为取代块一定不能取代参数
//它会报异常:URL query string "a={city}" must not have replace block. For dynamic query parameters use @Query.
//翻译:URL查询字符串“= {城市}”必须没有取代块。动态查询参数使用@Query。
//所以,{}取代块只能替换url而不能替换参数参数应该用@query
@GET("{parameters}?a=苏州市")
Call<AliAddrsBean> getIndexContentOne(
@Path("parameters") String parameters);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
所以,取代块只能取代url,不能取代参数,且@path的作用就是专职于取代块
调用的时候把参数传进来
Call<AliAddrsBean> requestInde = service.getIndexContentOne("geocoding");
- 1
- 1
@Query 键值对传参
看到那么多参数请求,肯定有简单的方法,单个参数添加
//看到那么多参数请求,肯定有简单的方法,单个参数添加
@POST("geocoding?")
Call<AliAddrsBean> getIndexContentTow(
@Query("a") String key1,
@Query("aa") String key2,
@Query("aaa") String key3
);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
调用:
Call<AliAddrsBean> requestInde = service.getIndexContentTow("苏州市","苏州市","苏州市");
- 1
- 1
@query的作用就相当于拼接字符串:a=上海市&aa=松江区&aaa=车墩镇
@QueryMap 参数集合
有时候我们参数很多,一个一个的用@query去传肯定不方便。
//看到那么多参数请求,肯定有简单的方法,多个参数添加使用map
@GET("geocoding?")
Call<AliAddrsBean> getIndexContentThree(
@QueryMap Map<String, Object> options
);
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
调用:
Map map = new HashMap();
map.put("a", "上海市");
map.put("aa", "黄浦区");
Call<AliAddrsBean> requestInde = service.getIndexContentThree(map);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
@Body 请求体
我们可以把参数封装成一个实体类,然后传过去。这么做比map传参更加方便。
//可以通过@Body注解指定一个对象作为Http请求的请求体
//类似于一二三级城市的参数都放在的请求体indexRequestBean里面
@POST("geocoding?")
Call<AliAddrsBean> getIndexContentFour(
@Body IndexRequestBean indexRequestBean);
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
调用
IndexRequestBean indexRequestBean = new IndexRequestBean();
indexRequestBean.setA("上海市");
indexRequestBean.setAa("松江区");
indexRequestBean.setAaa("车墩镇");
Call<AliAddrsBean> requestInde = service.getIndexContentFour(indexRequestBean);
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
这种做法得到的结果一样,但是方便了许多。
@FormUrlEncoded 和@Field 发送表单数据
@FormUrlEncoded注解的时候,将会发送form-encoded数据
//函数也可以声明为发送form-encoded(表单形式)和multipart(多部分)数据。
//当函数有@FormUrlEncoded注解的时候,将会发送form-encoded数据,
//每个键-值对都要被含有名字的@Field注解和提供值的对象所标注(这他妈是绕口令吗?)
//每个键值对的写法都是用注解@field标识的,表单形式的数据
@FormUrlEncoded
@POST("geocoding?")
Call<AliAddrsBean> getIndexContentFive(
@Field("a") String city,
@Field("aa") String citys,
@Field("aaa") String cityss
);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
调用:
Call<AliAddrsBean> requestInde = service.getIndexContentFive("苏州市","苏州市","苏州市");
- 1
- 1
@Multipart和@Part 发送字节流数据
我们有时候要在发送请求前规定数据的编码格式,那么我们就可以用这个注解来解决。
//当函数有@Multipart注解的时候,将会发送multipart数据,
// Parts都使用@Part注解进行声明
//Multipart parts要使用Retrofit的众多转换器之一或者实现RequestBody来处理自己的序列化。
//这个可以用于传文件,可以改变传值的编码,默认utf_8
@Multipart
@POST("geocoding?")
Call<AliAddrsBean> getIndexContentSix(
@Part("a") RequestBody city,
@Part("aa") RequestBody citya,
@Part("aaa") RequestBody cityaa);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可能这样看不太懂,那么我们就看看如何调用的就懂了
RequestBody requestBody1 = RequestBody.create(MediaType.parse("UTF-8"), "苏州市");//如果传值为null,则默认utf——8
// RequestBody requestBody2 = RequestBody.create(MediaType.parse("UTF-8"), "苏州市");
// RequestBody requestBody3 = RequestBody.create(MediaType.parse("UTF-8"), "苏州市");
// Call<AliAddrsBean> requestInde = service.getIndexContentSix(requestBody1,requestBody2,requestBody3);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
当然,既然是requestbody类型,我们就可以用它来穿文件了。
RequestBody requestBody1 = RequestBody.create(MediaType.parse("UTF-8"), new File("aaa"));//如果传值为null,则默认utf——8
- 1
- 2
- 1
- 2
@Headers给函数设置header
对于给一个请求设置header我在日志中看到打印了header的内容。
如果我们每次都要给服务器一些固定参数,,例如版本号,请求接口版本,key等。我们就可以用它来设置在http请求的头里。
//可以使用@Headers注解给函数设置静态的header
@Headers({"key:web_service_key","web_vsersion:1.01","app_version:1.02"})
@GET("geocoding?")
Call<AliAddrsBean> getIndexContentSeven(
@Query("a") String city
);
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
调用和get请求没有区别。是不是很方便?
@Header 单个参数的header
我感觉这个没什么卵用。。。
不过还是贴出来使用方法
//可以使用@Header注解动态的更新一个请求的header。必须给@Header提供相应的参数,
//如果参数的值为空header将会被忽略,否则就调用参数值的toString()方法并使用返回结果
@GET("geocoding?")
Call<AliAddrsBean> getIndexContentNine(
@Header("a") String city
);
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
适配Rxjava
这么多的Retrofit都是返回的call,我们应该如何适配Rxjava呢?
Retrofit默认是可以适配Rxjava的。
所以我们要做的就是:
//我们适配Rxjava的时候,只需要将返回结果的call变成Rxjava的被订阅者Observable即可
@GET("geocoding?")
Observable<AliAddrsBean> getIndexContentEleven(
@Query("a") String city);
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
调用:
Observable<AliAddrsBean> requestInde = service.getIndexContentEleven("苏州市");
- 1
- 1
我们得到了带有网络请求数据的Observable对象后我们就可以依照Rxjava来做一系列的响应式编程了。
这里还有一个坑,先看我们如何处理数据
(以下有坑)
requestInde.subscribe(new Action1<AliAddrsBean>() {
@Override
public void call(AliAddrsBean aliAddrsBean) {
//这里我只返回了成功的结果
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们直接给观察者或者叫订阅者发送消息“嘿!网络请求的数据回来了!”
这样做看似没问题,但是在我实际运行中却报错了。
异常表示数据处理需要在非ui线程。
那么就很了然了。我们得到requestInde的对象后要在非ui线程中操作。
以下代码已经填坑
Observable<AliAddrsBean> requestInde = service.getIndexContentEleven("苏州市");
requestInde.subscribeOn(Schedulers.newThread())//这里需要注意的是,网络请求在非ui线程。如果返回结果是依赖于Rxjava的,则需要变换线程
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<AliAddrsBean>() {
@Override
public void call(AliAddrsBean aliAddrsBean) {
//这里我只返回了成功的结果
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们要变换线程,处理数据的时候开一个新线程。返回结果就应该在主线程了。这里写android的ui线程就是用到的Rxandroid中的方法。
Observable.zip打包多个网络请求
有时候我们需要边加载数据边下载某个图片或者音乐甚至是各种文件。那么这个时候我们就要把多个网络请求打包起来一起发出去了。
那么这个时候我们就要借助Observable.zip这个操作符了。
先来看如何实现的
Observable.zip(
service.getIndexContentEleven("苏州市"),//第一个Observable对象
service.getIndexContentEleven("上海市"),//第二个Observable对象
new Func2<AliAddrsBean, AliAddrsBean, String>() {//function1中传入的是《1,第一个Observable对象;2,第二个Observable对象;3,返回类型》
@Override
public String call(AliAddrsBean o, AliAddrsBean o2) {
return o.getLat() + ":" + o2.getLat();
}
})
.subscribeOn(Schedulers.newThread())//这里需要注意的是,网络请求在非ui线程。如果返回结果是依赖于Rxjava的,则需要变换线程
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
Log.i("test", "retrofit error " + e.getMessage());
}
@Override
public void onNext(String s) {//传入的是function1中返回的对象
Log.i("test", "retrofit结果1" + s);
}
});
- 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
- 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
我们看zip这个操作符的源码可以得知,它能够封装两个被订阅者(Observable)对象。
并且默认还需要实现一个得到这两个Observable对象中的数据后的二次处理方法(Func2),把这两个对象的结果处理成一个然后发送给观察者(subscribe)。
一开始,并不知道哪个参数对应哪个,看了源码后才搞清楚。方法中我在注释中已经写的很清楚了。
这样我们就把两个网路请求同时进行,然后返回的结果处理成一个拿出来。达到了打包多个网络请求的目的。
添加okhttp委托
因为Retrofit内置okhttp,所以我们也可以为它设置一个委托okhttp对象。
具体委托方式:
Retrofit retrofit = new Retrofit
.Builder()
.baseUrl(url)
.client(client)//添加一个okhttp的委托
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
那么这个委托我们能做什么呢?
这里我写了一个okhttp的拦截器
//使用OkHttp拦截器可以指定需要的header给每一个Http请求
OkHttpClient client = new OkHttpClient();
//网络拦截器
client.networkInterceptors().add(new Interceptor() {
@Override
public com.squareup.okhttp.Response intercept(Chain chain) throws IOException {
com.squareup.okhttp.Response response = chain.proceed(chain.request());
return response;
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
我们可以做一个okhttp的拦截器,可以打印日志,可以clone 。反正发挥自己的想象,为正在请求的事件做一些额外的事情。
总结
写到这里,这个强强联合的框架已经写完了。
其实我们可以感觉到,这个框架主要是Retrofit2.0+RxJava+RxAndroid+OkHttp
其中RxAndroid的目的可能就是在android中使用的时候让网络请求回到主线程。
而Retrofit2.0+RxJava+OkHttp则充分的让这个网络请求框架变得如此之简单、之响应式、之模块化。
我们用注解去发送请求,简单、简洁;用Rxjava来让代码块状化,逻辑更清晰;网络请求有okhttp作为强大的后台可以很好的完成各种请求。
其中还有很多没有讲到的,比如Rxjava的RxBus和RxBinding方面。okhttp的更多操作。
因为Retrofit和okhttp一样,不支持下载进度的回调。所以之类附上另一位同行的博客来解决这个问题,顺便自己也学习学习。
解决Retrofit文件下载进度显示问题
(function () {('pre.prettyprint code').each(function () {
var lines = (this).text().split(′\n′).length;var numbering = $('
(this).addClass(′has−numbering′).parent().append( numbering);
for (i = 1; i <= lines; i++) {
numbering.append( ('
- ').text(i));
};
$numbering.fadeIn(1700);
});
});