美文网首页小技巧RxJava + RetrofitAndroid
Retrofit + RxJava + OkHttp 让网络请求

Retrofit + RxJava + OkHttp 让网络请求

作者: 依然范特稀西 | 来源:发表于2016-11-17 23:45 被阅读21388次

    前面一篇文章讲了一下Retrofit+ RxJava 请求网络的一些基本用法,还没有看过的可以去看一下Retrofit + RxJava + OkHttp 让网络请求变的简单-基础篇,正如标题所说的,Retrofit+RxJava 是让我们的网络请求变得简单,代码精简。通过前一篇文章,我们感觉写一个请求还是有点麻烦,作为程序员,我们的目标就是“偷懒”,绝不重复搬砖。因此我们还需要封装一下,来简化我们使用,接下来进入正题。

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

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

    /*
    * 
    * Created by zhouwei on 16/11/9. 
    */
    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 接口。代码如下:

    /*
     *
     * Created by zhouwei on 16/11/10. 
     */
    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 类,代码如下:

    /*
    * 
    *
    * 剥离 最终数据 
    * Created by zhouwei on 16/11/10. 
    */
    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<BaseResponse<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){      
          //错误处理   
         }  
      }
    }
    

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

    六,添加公共参数

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

    /*
     *
     * 拦截器
     *
     * 向请求头里添加公共参数 
     * Created by zhouwei on 16/11/10. 
     */
    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);
    

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

    ** 好了,以上一个简易的网络请求库就封装得差不多了,完整代码请戳Retrofit + RxJava +OkHttp 简易封装基本上能满足项目中的网络请求,由于项目中暂时没有文件上传下载的需求,这一块还没有添加,后面有时间会补充这一块的东西。**

    封装的类放在http包下:


    包结构.png

    最后放几张Demo示例的效果图:(数据来自干货集中营)
    重点是看妹纸!!!(滑稽脸)

    福利列表.png

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

    电影列表.png

    **以上就是封装的全部内容,还没有用Retrofit 的,赶快用上它来改造你想网络请求库吧!!! **

    参考博客:
    RxJava 与 Retrofit 结合的最佳实践

    相关文章

      网友评论

      • 4bf871c5ad39:写的很清晰,很容易理解。另外我有几个问题想请教下:
        1.如果在一个activity中发出多个不同的请求,我不太喜欢用多个匿名类的方式接收处理数据,那么通过实现接口来直接在activity中接收处理怎么样,这样的话实现action1接口这种方式如何,或者有没有更好的方式;
        2.多个不同请求统一处理的情况下,为方便统一处理的时候识别,每个请求是不是可以加入个识别码requestCode,随call()返回,方便分别处理。这样的话怎么弄合适呢
      • 彻儿:这个上传图片文件怎么传呢?后台给的参数类型就是File
      • 任荣军:你好,andorid新手,直接拿demo在项目里用,同样想知道公共参数中的token如何更新
        最好帮忙指点下在哪里做token过期判断并更新公共参数
        依然范特稀西:那就重新创建Retrofit吧
      • 08_carmelo:写的不错,问几个问题:
        1. 文中说道:“一个Loader里面可以处理多个Api 接口”
        这样确实很好,但实际中一个模块不只一个人开发。

        2.在Activity中发起请求获取结果,为何不继续封装,把data和error封装成一个Result,而要分成两个call,这样代码不是更简洁吗?

        3. MovieLoader是业务层来写的,业务层不关心MovieService 是怎么实例化的,换句话说,每个loader都要写这么一句:mMovieService = RetrofitServiceManager.getInstance().create(MovieService.class); 我觉得还可以再封装,对业务层隐藏实例化细节并且用反射调用getTop250这个方法
        依然范特稀西:@08_carmelo 1, 多人协作定好规范就好,2,activity 把data 和 error 封装成Result 多余了,文中是配合RxJava 写的,RxJava 有接受一个Call,2个Call 和3个call 的重载方法,但是一半选2个call 的,第2个call 做错误处理 , 3,确实,mMovieService = RetrofitServiceManager.getInstance().create(MovieService.class) 每一个Loader 都写确实麻烦,可以放到BaseLoader 中,用泛型,比如这样:
        public class BaseLoader<T> implements ILoader {
        private T mService;
        public BaseLoader(Class<T> tClass) {
        //通过Retrofit 获取 Service
        mService = RetrofitServiceManager.getInstance().create(tClass);
        }

        public T getService() {
        return mService;
        }

        }
        08_carmelo:坐等解惑...
      • 048099cf6af7:这个错误code无法打印啊,是有问题吗
      • 593d81b665f3:请问如果接口返回的是两个List集合 那么在getMovie这个方法里面应该怎么写呢?
      • wsxc:你好,PayLoad怎么使用呢,按照你给的方法一直报数据类型不一致
        半理想主义:@依然范特稀西 这里确实有问题,不能传BaseResponse<List<Movie>>,BaseResponse不需要传,而是返回什么值,就传入什么值
        wsxc:@依然范特稀西 map
        (rx.functions.Func1<? super MapModel,? extends java.util.List<com.yanmaiw.model.MapModel>>)
        in Observable cannot be applied
        to
        (com.yanmaiw.http.PayLoad<java.util.List<com.yanmaiw.model.MapModel>>),我按照你给的方式写的,这是他报的错误
        依然范特稀西:@wsxc map 一下就行:举个例子:observe(getService().getVerifCode(mobile, voice).map(new PayLoad<VerifyCode>()));
      • 简书无畏他祖先:报错:
        Process: com.xxx.xxx.xxx, PID: 28163
        java.lang.IllegalArgumentException: Unable to create call adapter for class rx.Observable
        for method UserService.login
      • ba64f76a1cfb:如果想要更新请求的公众参数的值需要怎么实现呢
      • flyer_C:文章代码和Demo中的这部分代码不一样。
        1、第五部分的
        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;
        }
        });
        }

        2、
        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());
        if(throwable instanceof Fault){
        Fault fault = (Fault) throwable;
        if(fault.getErrorCode() == 404){
        //错误处理
        }else if(fault.getErrorCode() == 500){
        //错误处理
        }else if(fault.getErrorCode() == 501){
        //错误处理
        }
        }
        }
        });

        }
        3、
        addSubscription(subscription);
        依然范特稀西:@flyer_C Demo 中没有用PayLoad ,是因为这个接口返回的格式不是按照标准的 msg,code,data 来返回的,这个不影响哈,按照文中讲的方法用是没问题的哈
        flyer_C:源码里没用到PayLoad吧?
      • 半颐:思路很清晰,学习了,谢谢。。。
        依然范特稀西:@半颐 多谢支持
      • 68a17e0a055b:剥离数据的PayLoad类,在您的代码中没有使用到啊?麻烦看一下
      • 5014161d46ba:可是我们项目服务器接口,返回的不是这种 {
        "status": 200,
        "message": "成功",
        "data": {}
        }统一格式,有时返回的是一个数组,有时返回的是一个对象,有时本应该是数组结果返回的对象,请问这种情况怎么处理?
        依然范特稀西:@坚果haah 这种情况不好处理,叫后端返回接口需要的类型吧,接口改返回什么类型就是什么类型,不能要求返回数组却返回个对象。
      • X1a0Yu_:学习了,谢谢大佬:+1:
        midfisher:拦截器那一段,Builder用static会报错的吧,
        X1a0Yu_:@依然范特稀西 有,谢谢
        依然范特稀西:@0502Leeyuu丶 谢谢你的赞赏,对你有帮助就好
      • dlihasa:发现你的这些博客思路和总结都很不错,很腻害!
        依然范特稀西:@DHASA2017 服务端有获取参数的方法,不同的后端语言可能有点不一样,Java 后端 直接在Response中根据参数名取就行,其他的语言不是很了解
        dlihasa:@依然范特稀西 不客气,刚才试了下,项目中请求服务器端的参数格式是json,那么添加公共参数的部分比如像userid这种就需要服务端去格外获取吗?比如每个请求都加了版本号之类的参数。
        依然范特稀西:@DHASA2017 谢谢
      • Evan_we:源码能发一下吗老哥
        依然范特稀西:https://github.com/pinguo-zhouwei/RetrofitRxJavaDemo
        Jsonjia:@依然范特稀西 哪有github连接。没看到啊
        依然范特稀西:文末有Github 链接哈
      • 牵着驼羊看简书:周伟,厉害啊
        依然范特稀西:@骑着上帝去逛街 😄😄
      • SoledadOne:你好!请问一下在加载瀑布流的时候,出现了空指针,导致无法正常启动Activity,最后程序直接崩溃,在setManger时出错,代码跟你的一样,目前我问题还没找出来,希望你解答一下
        SoledadOne:@依然范特稀西
        mRecyclerView.setLayoutManager(manager);
        就是这一行代码出现空指针
        SoledadOne:@依然范特稀西
        04-18 13:25:33.394 1032-1032/? E/AndroidRuntime: FATAL EXCEPTION: main
        java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hp.retrofitdemo/com.example.hp.retrofitdemo.gank.GankActivity}: java.lang.NullPointerException
        依然范特稀西:@Soledadone 哪儿报的空指针啊?你把错误信息贴出来看下呢
      • 8039c10659c9:谢谢分享
      • CrazyCarrot:您好!我在第五部分统一结果时总是报错误,Observable 不能应用,代码和您的一样感觉思路也对就是报错,感觉是Observable的问题。后来发现您的Demo中也不可以。而且您也没有用到这个类,希望您看一下~ :relaxed:
        书剑剑书: @field("userId") String userId 的参数 可以为其他类型吗,比如数组?可以吗?
        依然范特稀西:上面示例中的User 就是BaseResponse中的data
        依然范特稀西:@_Lv 报什么错呢?demo 中给的两个网络请求没有统一的返回格式,所以没有用,加进来挺简单的啊,比如返回的格式和你定义的BaseResponse 一样的话,在Service 里返回Obserble的的返回值用BaseResponse 包一层,最后用map 操作符,传入一个PayLoad 剥离数据就好了。比如:
        @FormUrlEncoded
        @POST("/api/getUser")
        Observable<BaseResponse<User>> getUserInfo(@Field("userId") String userId);
        然后用PayLoad剥离最终需要的数据

        public Observable<User> getUserInfo(String userId){
        return observe(mGankService.getUserInfo(userId)).map(new PayLoad<User>());
        }

      本文标题:Retrofit + RxJava + OkHttp 让网络请求

      本文链接:https://www.haomeiwen.com/subject/htzdpttx.html