重拾Android之路之Retrofit+RxJava+OkHttp


引言

Android项目必备基本轮子--------异步网络请求框架。

先不考虑在手项目的进度,也不管UI组件的深探,先来将Restful客户端的轮子造起来!

现在Android 市面上很火的当然是 RetrofitRxJava + OkHttp, 功能强大,简单易用,因此选用这套方案来改造网络库。


简介:

Retrofit: Retrofit是Square公司开发的一款针对Android 网络请求的框架。底层基于OkHttp实现,OkHttp 已经得到了Google 官方的认可。Retrofit官网

OkHttp: 也是Square 开源的网络请求库

RxJava:RxJava在 GitHub 主页上的自我介绍是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)。这就是 RxJava ,概括得非常精准。总之就是让异步操作变得非常简单。

各自的职责:Retrofit 负责请求的数据和请求的结果,使用接口的方式呈现,OkHttp 负责请求的过程,RxJava 负责异步,各种线程之间的切换。

RxJava + Retrofit + okHttp 已成为当前Android 网络请求最流行的方式。


分别实例介绍

一,Retrofit 写一个网络请求

以获取豆瓣 Top250 榜单为例,地址:https://api.douban.com/v2/movie/

  1. 首先,要使用Retrofit ,你肯定需要把它的包引入,在你的build.gradle文件中添加如下配置:
//下面两个是RxJava 和RxAndroid 
compile 'io.reactivex:rxjava:1.1.0' 
compile 'io.reactivex:rxandroid:1.2.0'

compile 'com.squareup.retrofit2:retrofit:2.4.0'//retrofit   
compile 'com.squareup.retrofit2:converter-gson:2.4.0'//转换器,请求结果转换成Model 
compile 'com.squareup.retrofit2:adapter-rxjava:2.4.0'//配合Rxjava 使用
  1. 创建一个Retrofit 实例,并且完成相关的配置
public static final String BASE_URL = "https://api.douban.com/v2/movie/";
Retrofit retrofit = new Retrofit.Builder() 
       .baseUrl(BASE_URL) 
       .addConverterFactory(GsonConverterFactory.create())
       .build();

说明:配置了接口的baseUrl和一个converter,GsonConverterFactory 是默认提供的Gson 转换器,Retrofit 也支持其他的一些转换器,详情请看官网Retrofit官网

  1. 创建一个接口 ,代码如下:
public interface MovieService { 

 //获取豆瓣Top250 榜单 
 @GET("top250")
 Call<MovieSubject> getTop250(@Query("start") int start,@Query("count")int count);
}
  1. 用Retrofit 创建 接口实例 MoiveService,并且调用接口中的方法进行网络请求,代码如下:
//获取接口实例
MovieService MovieService movieService = retrofit.create(MovieService.class); 
//调用方法得到一个Call 
Call<MovieSubject> call = movieService.getTop250(0,20);
 //进行网络请求 
call.enqueue(new Callback<MovieSubject>() {
       @Override 
       public void onResponse(Call<MovieSubject> call, Response<MovieSubject> response) { 
            mMovieAdapter.setMovies(response.body().subjects);     
            mMovieAdapter.notifyDataSetChanged(); 
       } 
      @Override 
      public void onFailure(Call<MovieSubject> call, Throwable t) { 
         t.printStackTrace(); 
      } 
});

以上是异步方式请求,还有同步方式execute(),返回一个Response,代码如下:

Response<MovieSubject> response = call.execute();

以上就是用Retrofit 完成了一个网络请求,获取豆瓣top250 榜单电影,效果图如下:

以上示例是用get方式完成,如果要使用post方式,我们只需要修改一下接口中的方法定义,如下:

public interface MovieService { 
        //获取豆瓣Top250 榜单 
       @FormUrlEncoded
       @POST("top250") 
       Call<MovieSubject> getTop250(@Field("start") int start, @Field("count") int count);
}

说明:使用POST请求方式时,只需要更改方法定义的标签,用@POST 标签,参数标签用 @Field 或者@Body或者FieldMap,注意:使用POST 方式时注意2点,1,必须加上 @FormUrlEncoded标签,否则会抛异常。2,使用POST方式时,必须要有参数,否则会抛异常, 源码抛异常的地方如下:

if (isFormEncoded && !gotField) { 
      throw methodError("Form-encoded method must contain at least one @Field."); 
}

以上就是一个使用Retrofit 完成一个网络请求的完整示例,其他标签使用方式请看官网Retrofit官网,官网用法也介绍的比较详细,此外,发现了一篇博客也介绍得比较详细,Retrofit用法详解

二,配合RxJava 使用
  1. 更改定义的接口,返回值不再是一个Call ,而是返回的一个Observble.
public interface MovieService { 
    //获取豆瓣Top250 榜单  
    @GET("top250") 
    Observable<MovieSubject> getTop250(@Query("start") int start, @Query("count")int count);
 }
  1. 创建Retrofit 的时候添加如下代码
addCallAdapterFactory(RxJavaCallAdapterFactory.create())
  1. 添加转换器Converter(将json 转为JavaBean)
addConverterFactory(GsonConverterFactory.create())
  1. Activity 或者 Fragment 中传入 Subscriber 建立订阅关系
Subscription subscription = movieService.getTop250(0,20) 
.subscribeOn(Schedulers.io()) 
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<MovieSubject>() { 
@Override
 public void onCompleted() { 

 } 
@Override 
public void onError(Throwable e) { 

} 
@Override
 public void onNext(MovieSubject movieSubject) { 
        mMovieAdapter.setMovies(movieSubject.subjects); 
        mMovieAdapter.notifyDataSetChanged(); 
   } 
});

以上是加入RxJava后的网络请求,返回不再是一个Call ,而是一个Observable, 在Activity/ Fragment 中传入一个Subscriber 建立订阅关系,就可以在 onNext 中处理结果了,RxJava 的好处是帮我处理线程之间的切换,我们可以在指定订阅的在哪个线程,观察在哪个线程。我们可以通过操作符进行数据变换。整个过程都是链式的,简化逻辑。其中FlatMap 操作符 还可以解除多层嵌套的问题。总之,RxJava 很强大,能帮我处理很多复杂的场景,如果熟练使用的话,那么能提升我们的开发效率。这里不打算讲RxJava 的内容,如果还不了解RxJava ,或者还对RxJava不熟悉的话,推荐几篇写很优秀的博客。

1,RxJava 的经典文章,扔物线的 给 Android 开发者的 RxJava 详解
2,关于RxJava 友好的文章
3,关于RxJava 友好的文章-进阶

三,加入 OkHttp 配置

通过OkHttpClient可以配置很多东西,比如链接超时时间缓存拦截器等等。代码如下:

// 创建 OKHttpClient 
OkHttpClient.Builder builder = new OkHttpClient.Builder(); 
     builder.connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS);//连接超时时间 
     builder.writeTimeout(DEFAULT_TIME_OUT,TimeUnit.SECONDS);//写操作 超时时间 
     builder.readTimeout(DEFAULT_TIME_OUT,TimeUnit.SECONDS);//读操作超时时间 

  // 添加公共参数拦截器 
BasicParamsInterceptor basicParamsInterceptor = new BasicParamsInterceptor.Builder() 
    .addHeaderParam("userName","")//添加公共参数 
    .addHeaderParam("device","") 
    .build(); 

 builder.addInterceptor(basicParamsInterceptor); 

// 创建Retrofit
 mRetrofit = new Retrofit.Builder() 
     .client(builder.build()) 
     .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 
     .addConverterFactory(GsonConverterFactory.create()) 
     .baseUrl(ApiConfig.BASE_URL) 
     .build();

以上只是配置了一些简单的项,如,连接超时时间,实际项目中,我们可能有一些公共的参数,如 :设备信息渠道Token之类的,每个接口都需要用,我们可以写一个拦截器,然后配置到OKHttpClient里,通过 builder.addInterceptor(basicParamsInterceptor) 添加,这样我们就不用每个接口都添加这些参数了。缓存也可以通过写一个拦截器来实现(后面文章再讲)。

以上就是Retrofit+RxJava+OkHttp实现网络请求的简单演示,如果每个接口都这么写的话,代码量太多,而且不优雅。所以还需要我们封装一下,接下来讲一下对网络请求框架的封装。

参考博客:
1,Retrofit用法详解
2,基于Retrofit、OkHttp、Gson封装通用网络框架
3, RxJava 与 Retrofit 结合的最佳实践


封装

一,创建一个统一生成接口实例的管理类RetrofitServiceManager

我们知道,每一个请求,都需要一个接口,里面定义了请求方法和请求参数等等,而获取接口实例需要通过一个Retrofit实例,这一步都是相同的,因此,我们可以把这些相同的部分抽取出来,代码如下:

/*
* 
*/
public class RetrofitServiceManager { 
  private static final int DEFAULT_TIME_OUT = 5;//超时时间 5s    
  private static final int DEFAULT_READ_TIME_OUT = 10;    
  private Retrofit mRetrofit;   
  private RetrofitServiceManager(){  
  
  // 创建 OKHttpClient      
  OkHttpClient.Builder builder = new OkHttpClient.Builder();      
  builder.connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS);//连接超时时间
  builder.writeTimeout(DEFAULT_READ_TIME_OUT,TimeUnit.SECONDS);//写操作 超时时间        
  builder.readTimeout(DEFAULT_READ_TIME_OUT,TimeUnit.SECONDS);//读操作超时时间  

  // 添加公共参数拦截器        
  HttpCommonInterceptor commonInterceptor = new HttpCommonInterceptor.Builder() 
               .addHeaderParams("paltform","android") 
               .addHeaderParams("userToken","1234343434dfdfd3434") 
               .addHeaderParams("userId","123445")      
               .build();        
  builder.addInterceptor(commonInterceptor);    
  
  // 创建Retrofit        
  mRetrofit = new Retrofit.Builder() 
               .client(builder.build())  
               .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 
               .addConverterFactory(GsonConverterFactory.create()) 
               .baseUrl(ApiConfig.BASE_URL)   
               .build();   
  } 

  private static class SingletonHolder{
    private static final RetrofitServiceManager INSTANCE = new RetrofitServiceManager();
  }

  /**
    * 获取RetrofitServiceManager
    * @return
    */   
  public static RetrofitServiceManager getInstance(){  
    return SingletonHolder.INSTANCE; 
  }  
  /** 
    * 获取对应的Service 
    * @param service Service 的 class     
    * @param <T>    
    * @return  
    */  
  public <T> T create(Class<T> service){ 
       return mRetrofit.create(service);    
  }
}

说明:创建了一个RetrofitServiceManager类,该类采用单例模式,在私有的构造方法中,生成了Retrofit 实例,并配置了OkHttpClient和一些公共配置。提供了一个create()方法,生成接口实例,接收Class范型,因此项目中所有的接口实例Service都可以用这个来生成,代码如下:

mMovieService = RetrofitServiceManager.getInstance().create(MovieService.class);

通过create()方法生成了一个MovieService

二,创建接口,通过第一步获取实例

上面已经有了可以获取接口实例的方法因此我们需要创建一个接口,代码如下:

public interface MovieService{  
  //获取豆瓣Top250 榜单   
  @GET("top250")    
  Observable<MovieSubject> getTop250(@Query("start") int start, @Query("count")int count);   

  @FormUrlEncoded    
  @POST("/x3/weather")   
  Call<String> getWeather(@Field("cityId") String cityId, @Field("key") String key);
}

好了,有了接口我们就可以获取到接口实例了mMovieService

三,创建一个业务Loader ,如XXXLoder,获取Observable并处理相关业务

解释一下为什么会出现Loader ,我看其他相关文章说,每一个Api 都写一个接口,我觉得这样很麻烦,因此就把请求逻辑封装在在一个业务Loader 里面,一个Loader里面可以处理多个Api 接口。代码如下:

public class MovieLoader extends ObjectLoader { 
   private MovieService mMovieService; 
   public MovieLoader(){  
      mMovieService = RetrofitServiceManager.getInstance().create(MovieService.class);
    }  
  /** 
    * 获取电影列表 
    * @param start  
    * @param count    
    * @return    
    */  
  public Observable<List<Movie>> getMovie(int start, int count){  
      return observe(mMovieService.getTop250(start,count)) 
               .map(new Func1<MovieSubject, List<Movie>>() {   
         @Override 
           public List<Movie> call(MovieSubject movieSubject) {   
             return movieSubject.subjects;     
       }   
     }); 
   }   

public Observable<String> getWeatherList(String cityId,String key){    
        return observe(mMovieService.getWeather(cityId,key))
       .map(new Func1<String, String>() {     
       @Override      
       public String call(String s) {
           //可以处理对应的逻辑后在返回
            return s;    
       } 
     });
}

 public interface MovieService{   
      //获取豆瓣Top250 榜单  
      @GET("top250")       
     Observable<MovieSubject> getTop250(@Query("start") int start, @Query("count")int count);   

     @FormUrlEncoded   
     @POST("/x3/weather")    
    Call<String> getWeather(@Field("cityId") String cityId, @Field("key") String key);   
 }
}

创建一个MovieLoader,构造方法中生成了mMovieService,而Service 中可以定义和业务相关的多个api,比如:例子中的MovieService中,可以定义和电影相关的多个api,获取电影列表、获取电影详情、搜索电影等api,就不用定义多个接口了。

上面的代码中,MovieLoader是从ObjectLoader 中继承下来的,ObjectLoader 提取了一些公共的操作。代码如下:

/** 
 *
 * 将一些重复的操作提出来,放到父类以免Loader 里每个接口都有重复代码 
 * Created by zhouwei on 16/11/10.
 * 
 */
public class ObjectLoader {   
 /**
   * 
   * @param observable     
   * @param <T>   
   * @return    
   */   
 protected  <T> Observable<T> observe(Observable<T> observable){    
    return observable
      .subscribeOn(Schedulers.io())          
      .unsubscribeOn(Schedulers.io())  
      .observeOn(AndroidSchedulers.mainThread());  
  }
}

相当于一个公共方法,其实也可以放在一个工具类里面,后面做缓存的时候会用到这个父类,所以就把这个方法放到父类里面。

四,Activity/Fragment 中的调用
创建Loader实例
mMovieLoader = new MovieLoader();

通过Loader 调用方法获取结果,代码如下:

/*
 *
 * 获取电影列表 
 */
private void getMovieList(){ 
   mMovieLoader.getMovie(0,10).subscribe(new Action1<List<Movie>>() {   
     @Override   
     public void call(List<Movie> movies) {   
         mMovieAdapter.setMovies(movies);        
         mMovieAdapter.notifyDataSetChanged();      
        } 
     }, new Action1<Throwable>() {    
     @Override       
     public void call(Throwable throwable) {    
        Log.e("TAG","error message:"+throwable.getMessage());     
     }  
   });
}

以上就完成请求过程的封装,现在添加一个新的请求,只需要添加一个业务Loader 类,然后通过Loader调用方法获取结果就行了,是不是方便了很多?但是在实际项目中这样是不够的,还能做进一步简化。

五,统一处理结果和错误
1,统一处理请求结果

现实项目中,所有接口的返回结果都是同一格式,如:

{
 "status": 200,
 "message": "成功",
 "data": {}
}

我们在请求api 接口的时候,只关心我们想要的数据,也就上面的data,其他的东西我们不太关心,请求失败的时候可以根据status判断进行错误处理,所以我们需要包装一下。首先需要根据服务端定义的JSON 结构创建一个BaseResponse 类,代码如下:

/*
*
* 
* 网络请求结果 基类 
* Created by zhouwei on 16/11/10. 
*/
public class BaseResponse<T> {   
  public int status;  
  public String message;    
  public T data;    
  public boolean isSuccess(){   
     return status == 200;  
  }
}

有了统一的格式数据后,我们需要剥离出data 返回给上层调用者,创建一个PayLoad 类,代码如下:

/*
* 剥离 最终数据 
*/
public class PayLoad<T> implements Func1<BaseResponse<T>,T>{    
   @Override 
   public T call(BaseResponse<T> tBaseResponse) {
   //获取数据失败时,包装一个Fault 抛给上层处理错误
      if(!tBaseResponse.isSuccess()){ 
           throw new Fault(tBaseResponse.status,tBaseResponse.message);  
      }    
   return tBaseResponse.data;  
   }
}

PayLoad 继承自Func1,接收一个BaseResponse<T> , 就是接口返回的JSON数据结构,返回的是T,就是data,判断是否请求成功,请求成功返回Data,请求失败包装成一个Fault 返回给上层统一处理错误。在Loader类里面获取结果后,通过map 操作符剥离数据。代码如下:

public Observable<List<Movie>> getMovie(int start, int count){ 
  return observe(mMovieService.getTop250(start,count))        
    .map(new PayLoad<List<Movie>>());
}
2,统一处理错误

在PayLoad 类里面,请求失败时,抛出了一个Fault 异常给上层,我在Activity/Fragment 中拿到这个异常,然后判断错误码,进行异常处理。在onError () 中添加代码如下:

public void call(Throwable throwable) {  
  Log.e("TAG","error message:"+throwable.getMessage());  
  if(throwable instanceof Fault){     
    Fault fault = (Fault) throwable;    
    if(fault.getErrorCode() == 404){     
       //错误处理 
    }else if(fault.getErrorCode() == 500){   
         //错误处理  
    }else if(fault.getErrorCode() == 501){      
      //错误处理   
    }  
  }
}

以上就可以对应错误码处理相应的错误了。

六,添加公共参数

在实际项目中,每个接口都有一些基本的相同的参数,我们称之为公共参数,比如:userIduserTokenuserName,deviceId等等,我们不必要,每个接口都去写,这样就太麻烦了,因此我们可以写一个拦截器,在拦截器里面拦截请求,为每个请求都添加相同的公共参数。拦截器代码如下:

/*
 * 拦截器
 * 向请求头里添加公共参数 
 */
public class HttpCommonInterceptor implements Interceptor {    
    private Map<String,String> mHeaderParamsMap = new HashMap<>();  
    public HttpCommonInterceptor() {}    
    
    @Override
    public Response intercept(Chain chain) throws IOException {    
    Log.d("HttpCommonInterceptor","add common params");     
    Request oldRequest = chain.request();    
    // 添加新的参数,添加到url 中  
    /* HttpUrl.Builder authorizedUrlBuilder = oldRequest.url().newBuilder()       
         .scheme(oldRequest.url().scheme())   
         .host(oldRequest.url().host());*/ 
    // 新的请求   
    Request.Builder requestBuilder =  oldRequest.newBuilder(); 
    requestBuilder.method(oldRequest.method(), oldRequest.body()); 

    //添加公共参数,添加到header中        
    if(mHeaderParamsMap.size() > 0){       
       for(Map.Entry<String,String> params:mHeaderParamsMap.entrySet()){  
          requestBuilder.header(params.getKey(),params.getValue());       
       }    
    }    
    Request newRequest = requestBuilder.build();   
    return chain.proceed(newRequest);  
    } 
 
    public static class Builder{      
      HttpCommonInterceptor mHttpCommonInterceptor;    
      public Builder(){      
        mHttpCommonInterceptor = new HttpCommonInterceptor();     
      }     
  
      public Builder addHeaderParams(String key, String value){      
        mHttpCommonInterceptor.mHeaderParamsMap.put(key,value);   
        return this;   
      }       

      public Builder  addHeaderParams(String key, int value){   
         return addHeaderParams(key, String.valueOf(value)); 
      }  
      
      public Builder  addHeaderParams(String key, float value){ 
           return addHeaderParams(key, String.valueOf(value));  
      }      
    
      public Builder  addHeaderParams(String key, long value){  
          return addHeaderParams(key, String.valueOf(value));      
      }    
    
      public Builder  addHeaderParams(String key, double value){    
        return addHeaderParams(key, String.valueOf(value));    
      } 
      
      public HttpCommonInterceptor build(){ 
           return mHttpCommonInterceptor;     
      } 
    }
}

以上就是添加公共参数的拦截器,在RetrofitServiceManager 类里面加入OkHttpClient 配置就好了。代码如下:

// 添加公共参数拦截器
HttpCommonInterceptor commonInterceptor = new HttpCommonInterceptor.Builder()     
       .addHeaderParams("paltform","android")   
       .addHeaderParams("userToken","1234343434dfdfd3434") 
       .addHeaderParams("userId","123445")      
       .build();
builder.addInterceptor(commonInterceptor);

这样每个请求都添加了公共参数。

封装的类放在http包下:

电影列表:(数据来自豆瓣)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容