关于RxJava原理分析,请参考仍物线写的文章—-给 Android 开发者的 RxJava 详解。本文不对原理作过多的分析,从最快上手的角度,让开发者使用起来,当我们有实践经验后回过头来看原理分析会更清晰。
本系列共有三篇文章,分别关于Rxjava的基础使用(最快,最实用),Retrofit使用(Github上star达22k+,安卓领域排名第一),最后是RxCache缓存(大部分app都支持离线查看功能)。
这是第二篇,网络请求框架Retrofit的使用,包括封装,日志打印,添加头部信息,Get和Post。本来想把RxCache作为第三篇文章,但是想想没必要,就作为第二篇的一部分,所以这个系列暂时就只有这两篇文章。
首先添加引用
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
第三个将服务器返回的数据用Gson转换,第五个是日志拦截器,用来打印日志。
因为Retrofit是基于Okhttp的,所以先获取OkHttp,
private OkHttpClient getClient() {
HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.d("resHttp",message);//打印服务器返回的内容
}
});
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//BODY表示显示服务器返回的内容体,还有其他级别比如Header
OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder().cacheControl(CacheControl.FORCE_NETWORK)
.addHeader("X-Requested-With", "XMLHttpRequest")//添加头部信息,比如session
// .addHeader("Cookie", "JSESSIONID="+ ZxlVar.mySession)
.build();
return chain.proceed(request);}})
.addInterceptor(loggingInterceptor).build();
return httpClient;
}
然后通过单例模式获取Retrofit对象,其中传入上面的httpClient
public Retrofit getRetrofit(String hostUrl)
{
if (mRetrofit==null)
mRetrofit=new Retrofit.Builder().baseUrl(hostUrl).client(getClient()) .addConverterFactory(GsonConverterFactory.create())//使用Gson来解析数据,这个可以自定义,但是一般不需要
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build();
return mRetrofit;
}
使用之前,先新建类用来存放所有的调用接口,并指明调用方式(Get/Post),声明参数类型及个数,以及要解析的格式,比如
@GET("/jt/getApkVersion.htm")
Observable<ApkUpdateEntity> getApkVersson(@Query("apkCode") String apkCode);
其对应的完整URL为:
http://app.xxx.com00/jt/getApkVersion.htm?apkCode=OperationTeminal
Post请求格式如下:
@POST("/jt/icCardManager/uploadIcCardRecord")
Observable<CardUpResEntity> upLoadCardData(@Query("terminalCode") String terminalCode
, @Query("icCardData") String data);
其对应的完整URL为:
http://app.xxx.com00/jt/icCardManager/uploadIcCardRecord?terminalCode=xxx&icCardData=xxx
服务器返回数据后,解析成ApkUpdateEntity格式,然后就可以调用里面的内容。涉及到私密性,这里使用网上公用接口
https://api.github.com/
不需要传参数,调用代码如下
MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("https://api.github.com/")//一定以 / 结尾
.create(MyApiProvider.class); myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子线程进行耗时处理 .observeOn(AndroidSchedulers.mainThread())//指定在UI线程更新UI
.subscribe(new Consumer<TestEntity>() {//指定按照TestEntity解析
@Override
public void accept(TestEntity testEntity) throws Exception {//TestEntity,这里是解析后的结果
//处理返回的结果 Toast.makeText(MainActivity.this,testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
}
});
接口声明为:
@GET("/")
Observable<TestEntity> getApkVersson();
如果将@GET(“/”) 换成@GET(“”)会报错 Missing either @GET URL or @Url parameter.
运行效果
至此就可以从服务器拿到相应的数据并做处理了。我们再看另外一个重要的话题–缓存。缓存至少有三个好处,一是减少网络请求次数,以降低对服务器的压力,二是缩短网络请求时间,从本地取数据肯定比从服务器取数据要快,三是网络情况差甚至断网后还可以查看数据,你现在还能看到断网了不能使用的app吗,很少了。既然缓存这么重要,那么一般是怎么做的呢?现在一般使用二级缓存,即内存缓存和硬盘缓存。内存没有数据,就去硬盘取,还没有,就从服务器取数据。注意:内存缓存会造成堆内存泄露,所有一级缓存通常要严格控制缓存的大小,一般控制在系统内存的1/4。这两种缓存方式分别对应ASimpleCache和DiskLruCache。大家可以去网上搜索,实现起来也不复杂,但是刚上手的人可能也要花不少时间。这都不重要,我要说的是,有了Retrofit支持的RxCache,前面那些东西都不要了,对,Retrofit已经帮我们实现了二级缓存,不用费老大的劲去自己实现,效果还不一定有人家的好。下面进入主题使用RxCache+Retrofit+RxJava实现二级缓存。
RxCache地址
先添加依赖
compile "com.github.VictorAlbertos.RxCache:runtime:1.8.0-2.x"
compile 'com.github.VictorAlbertos.Jolyglot:gson:0.0.3'
然后获取RxCache对象
public CacheProvider getCacheProvider()
{
if (mCacheProvider==null) {
mCacheProvider = new RxCache.Builder().persistence(MyRetrofitUtil.getCacheDir(getApplicationContext()), new GsonSpeaker()).using(CacheProvider.class);
}
return mCacheProvider;
}
其中CacheProvider类用于存放需要缓存的接口,
public interface CacheProvider {
@Expirable(false)//false表示内存不足系统回收时永远不回收
@LifeCache(duration = 60,timeUnit = TimeUnit.MINUTES)//60分钟有效时间
Observable<TestEntity> getApkVersson(Observable<TestEntity> obs);
}
注意CacheProvider中的接口名getApkVersson要与MyApiProvider 中的一致。
调用:
MyApiProvider iGetData= MyApplication.getInstance().getRetrofit("https://api.github.com/").create(MyApiProvider.class);
CacheProvider cache=MyApplication.getInstance().getCacheProvider();//获取CacheProvider对象
Observable<TestEntity> observable;
observable= cache.getApkVersson( iGetData.getApkVersson());//调用接口(带缓存功能)
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<TestEntity>() {
@Override
public void accept(@NonNull TestEntity testEntity) throws Exception { Toast.makeText(MainActivity.this,"RxCache:"+testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
}
});
运行效果
从演示中可以看到,关闭网络后依然可以拿到缓存数据展示。至此缓存功能已经实现,还有两个参数需要说明一下,
Observable<TestEntity> getApkVersson2(Observable<TestEntity> obs,EvictProvider evictProvider, DynamicKey dynamicKey);
//EvictProvider表示是否强制刷新,下拉刷新时就需要强制刷新,DynamicKey表示要缓存第几页数据,当不传此参数时默认缓存第一页
传值方式为 new EvictProvider(true/false),new DynamicKey(page)
。
还有一点就是为什么使用Gson来解析,因为有GsonFormate工具。使用非常方便, 开发必备,还没使用的赶紧去上手。操作效果如下:
当然,上面介绍的内容不复杂,但很实用,还有很多额外的东西没有介绍,我之所以没有介绍,是因为我项目中没有用到,项目中用到的知识点都介绍了。我想,如果我不加区分的把所有东西都介绍一遍,不仅文章会显得又臭又长,还让人抓不住重点,那样做又与官方的API文档有什么分别呢?这也是很多人写博客的一个通病,其实我也有,但我会有意识的避免。
过程中遇到的问题1:在没有网的情况下点击第一个按钮会报错并且崩溃
resHttp: <-- HTTP FAILED: java.net.UnknownHostException: Unable to resolve host "api.github.com": No address associated with hostname
io.reactivex.exceptions.OnErrorNotImplementedException: Unable to resolve host "api.github.com": No address associated with hostname```
而且配置文件已经配置网络权限<uses-permission android:name="android.permission.INTERNET" />
,上网搜了一下也没解决,不是说没有配置权限,就是说模拟器有问题,重启模拟器,还有的说是服务器有问题,还是只能靠自己。仔细看错误内容,其中OnErrorNotImplementedException提示说该异常主要是指OnError方法没有实现,OnError方法是哪里的呢。依稀记得重写观察者Observer时要重写OnNext(),OnComplete(),OnError()等方法,对,就是这个OnError()。实现它就不报错了。
MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("https://api.github.com/")//一定以 / 结尾
.create(MyApiProvider.class); myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子线程进行耗时处理 .observeOn(AndroidSchedulers.mainThread())//指定在UI线程更新UI
.subscribe(new Consumer<TestEntity>() {//指定按照TestEntity解析
@Override
public void accept(TestEntity testEntity) throws Exception {//TestEntity,这里是解析后的结果
//处理返回的结果 Toast.makeText(MainActivity.this,testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
}
});
修改后的代码为
MyApiProvider myApiProvider= MyApplication.getInstance().getRetrofit("http://api.github.com/")//一定以 / 结尾
.create(MyApiProvider.class);
myApiProvider.getApkVersson().subscribeOn(Schedulers.io())//指定在子线程进行耗时处理
.observeOn(AndroidSchedulers.mainThread())//指定在UI线程更新UI
.subscribe(new Observer<TestEntity>() {//new Observer()得到观察者,并订阅被观察者,调用时顺序相反
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(TestEntity testEntity) {
Toast.makeText(MainActivity.this,"NoCache:"+testEntity.getAuthorizations_url(),Toast.LENGTH_LONG).show();
}
@Override
public void onError(Throwable e) {
Toast.makeText(MainActivity.this,"NoCacheError:"+e.toString(),Toast.LENGTH_LONG).show();
}
@Override
public void onComplete() {
}
});
也就是将new Consumer<TestEntity>()
后面的内容替换掉,不能偷懒只重写OnNext()方法,如果要考虑异常的话,还有重写OnError()方法。
过程中遇到的问题2:在没有网的情况下,第一次启动app,先点击第二个按钮会报错并且崩溃,如果点击了第一个按钮,再点击第二个按钮则没问题。
其实这与问题1是同一个问题但是表现不一样,没有缓存时点击缓存按钮则去网络请求,此时没有网,就出现问题1的场景,而先点击按钮1后,数据被缓存了,再点击缓存按钮就去取缓存没有去网络请求,所以不报错。嗯,就是这样。
修改后的运行效果