Retrofit2 完全解析(一)

1.概述

     在对Android 开发中,我们都是从原生的 HttpUrlConnection到经典的 Apache公司的HttpClient,再到对前面这些网络基础框架的封装(比如VolleyAsyncHttpClient等)。Http请求相关开源框架还是很多的,今天我们讲解 Square 公司开源的Retrofit。Square 公司的框架总是一如既往的简洁优雅!Retrofit更是以其简易的接口配置、强大的扩展支持、优雅的代码结构受到大家的追捧。 
Retrofit是一个 RESTful 的 HTTP 网络请求框架的封装。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是Retrofit来完成的。Retrofit2.0 开始内置OkHttp,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作!
我们的应用程序通过 Retrofit请求网络,实际上是使用Retrofit接口层封装请求参数、Header、Url 等信息,之后由OkHttp完成后续的请求操作,在服务端返回数据之后,OkHttp将原始的结果交给Retrofit,后者根据用户的需求对结果进行解析的过程。

2.常用法

  • 一般的get、post请求
  • 动态url,动态参数设置,各种注解的使用
  • 上传文件(单文件,多文件上传等)
  • 下载文件等(这个不推荐retrofit去做,具体看下文)

此外,由于其内部提供了ConverterFactory用于对返回的requestBody进行转化和特殊的requestBody的构造,所以本文也包含:

  如何自定义ConverterFactory

  retrofit 源码分析

retrifit版本,在android studio中,加入:

compile 'com.squareup.retrofit2:retrofit:2.0.2'

相关开源框架地址:

 开源框架地址:https://github.com/square/retrofit 
 英文文档官网:http://square.github.io/retrofit/
 RxJava框架:https://github.com/ReactiveX/RxJava 
 okhttp框架:https://github.com/square/okhttp


3.用法示例

3.1 一般的get请求

retrofit在使用的过程中,需要定义一个接口对象,我们首先演示一个最简单的get请求,接口如下所示:

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsers();
}
可以看到有一个getUsers()方法,通过 @GET注解标识为get请求, @GET中所填写的value和 baseUrl组成完整的路径, baseUrl在构造retrofit对象时给出。

下面看如何通过retrofit完成上述的请求:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://192.168.31.242:8080/springmvc_users/user/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
IUserBiz userBiz = retrofit.create(IUserBiz.class);
Call<List<User>> call = userBiz.getUsers();
        call.enqueue(new Callback<List<User>>()
        {
            @Override
            public void onResponse(Call<List<User>> call, Response<List<User>> response)
            {
                Log.e(TAG, "normalGet:" + response.body() + "");
            }

            @Override
            public void onFailure(Call<List<User>> call, Throwable t)
            {

            }
        });
依然是构造者模式,指定了 baseUrlConverter.Factory,该对象通过名称可以看出是用于对象转化的,本例因为服务器返回的是json格式的数组,所以这里设置了 GsonConverterFactory完成对象的转化。这里可以看到很神奇,我们通过 Retrofit.create就可以拿到我们定义的 IUserBiz的实例,调用其方法即可拿到一个 Call对象,通过 call.enqueue即可完成异步的请求。具体retrofit怎么得到我们接口的实例的,以及对象的返回结果是如何转化的,我们后面具体分析。

      这里需要说明如下几点:

     (1)接口中的方法必须有返回值,且比如是Call<T>类型

     (2)addConverterFactory(GsonConverterFactory.create())这里如果使用gson,需要额外导入:

    

   compile 'com.squareup.retrofit2:converter-gson:2.0.2'
      当然除了gson以外,还提供了以下的选择:    
Gson: com.squareup.retrofit2:converter-gson
Jackson: com.squareup.retrofit2:converter-jackson
Moshi: com.squareup.retrofit2:converter-moshi
Protobuf: com.squareup.retrofit2:converter-protobuf
Wire: com.squareup.retrofit2:converter-wire
Simple XML: com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars
当然也支持自定义,你可以选择自己写转化器完成数据的转化,这个后面将具体介绍。

3.2动态URL访问 @PATH

retrofit非常适用于restful url的格式,那么例如下面这样的url:

//用于访问zhy的信息
http://192.168.1.102:8080/springmvc_users/user/wind
//用于访问lmj的信息
http://192.168.1.102:8080/springmvc_users/user/jxf
即通过不同的username访问不同用户的信息,返回数据为json字符串。那么可以通过retrofit提供的 @PATH注解非常方便的完成上述需求。

我们再定义一个方法:

public interface IUserBiz
{
    @GET("{username}")
    Call<User> getUser(@Path("username") String username);
}
可以看到我们定义了一个getUser方法,方法接收一个username参数,并且我们的 @GET注解中使用 {username}声明了访问路径,这里你可以把 {username}当做占位符,而实际运行中会通过 @PATH("username")所标注的参数进行替换。

代码如下:

//省略了retrofit的构建代码
Call<User> call = userBiz.getUser("zhy");
//Call<User> call = userBiz.getUser("lmj");
call.enqueue(new Callback<User>()
{

    @Override
    public void onResponse(Call<User> call, Response<User> response)
    {
        Log.e(TAG, "getUsePath:" + response.body());
    }

    @Override
    public void onFailure(Call<User> call, Throwable t)
    {

    }
});

3.3 查询参数设置@Query

看下面的url

http://baseurl/users?sortby=username
http://baseurl/users?sortby=id
Query其实就是 Url 中 ‘?’ 后面的 key-value

我们可以通过@Query注解方便的完成,我们再次在接口中添加一个方法:

public interface IUserBiz
{
    @GET("users")
    Call<List<User>> getUsersBySort(@Query("sortby") String sort);
}
访问的代码:

//省略retrofit的构建代码
Call<List<User>> call = userBiz.getUsersBySort("username");
//Call<List<User>> call = userBiz.getUsersBySort("id");
//省略call执行相关代码
ok,这样我们就完成了参数的指定,当然相同的方式也适用于POST,只需要把注解修改为 @POST即可。当面临参数很多的情况,我们就采用QueryMap!

3.4 POST请求体的方式向服务器传入json字符串@Body

我们app很多时候跟服务器通信,会选择直接使用POST方式将json字符串作为请求体发送到服务器,那么我们看看这个需求使用retrofit该如何实现。

public interface IUserBiz
{
 @POST("add")
 Call<List<User>> addUser(@Body User user);
}
提交的代码其实基本都是一致的:

//省略retrofit的构建代码
 Call<List<User>> call = userBiz.addUser(new User(1001, "jj", "123,", "jj123", "jj@qq.com"));
//省略call执行相关代码
可以看到其实就是使用 @Body这个注解标识我们的参数对象即可,那么这里需要考虑一个问题,retrofit是如何将user对象转化为字符串呢?下文将详细解释
下面对应okhttp,还有两种requestBody,一个是 FormBody,一个是 MultipartBody,前者以表单的方式传递简单的键值对,后者以POST表单的方式上传文件可以携带参数, retrofit也二者也有对应的注解

3.5 表单的方式传递键值对@FormUrlEncoded

这里我们模拟一个登录的方法,添加一个方法:

public interface IUserBiz
{
    @POST("login")
    @FormUrlEncoded
    Call<User> login(@Field("username") String username, @Field("password") String password);
}
访问的代码:

//省略retrofit的构建代码
Call<User> call = userBiz.login("zhy", "123");
//省略call执行相关代码
通过 @POST指明url,添加 FormUrlEncoded,然后通过 @Field添加参数即可, Field声明了表单的项,这样提交表单就跟普通的函数调用一样简单直接了,表单项不确定个数就试用FieldMap

3.6单文件上传@Multipart

public interface IUserBiz
{
    @Multipart
    @POST("register")
    Call<User> registerUser(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password);
}
这里 @MultiPart的意思就是允许多个 @Part了,我们这里使用了3个 @Part,第一个我们准备上传个文件,使用了 MultipartBody.Part类型,其余两个均为简单的键值对。

File file = new File(Environment.getExternalStorageDirectory(), "icon.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoRequestBody);

Call<User> call = userBiz.registerUser(photo, RequestBody.create(null, "abc"), RequestBody.create(null, "123"));
多个 @Part,每个Part对应一个RequestBody。

插入一个实验验证:

最初对于文件,也是尝试的@Part RequestBody,因为@Part("key"),然后传入一个代表文件的RequestBody,我觉得更加容易理解,后来发现试验无法成功,而且查了下issue,给出了一个很奇怪的解决方案,这里可以参考:retrofit#1063

给出了一个类似如下的方案:

public interface ApiInterface {
        @Multipart
        @POST ("/api/Accounts/editaccount")
        Call<User> editUser (@Header("Authorization") String authorization, @Part("file\"; filename=\"pp.png") RequestBody file , @Part("FirstName") RequestBody fname, @Part("Id") RequestBody id);
    }

可以看到对于文件的那个@Partvalue竟然写了这么多奇怪的东西,而且filename竟然硬编码了~~这个不好吧,我上传的文件名竟然不能动态指定。

为了文件名不会被写死,所以给出了最上面的上传单文件的方法,ps:上面这个方案经测试也是可以上传成功的。

恩,这个奇怪方案,为什么这么做可行,下文会给出非常详细的解释。

3.7 多文件上传@PartMap

public interface IUserBiz
 {
     @Multipart
     @POST("register")
      Call<User> registerUser(@PartMap Map<String, RequestBody> params,  @Part("password") RequestBody password);
}

这里使用了一个新的注解 @PartMap,这个注解用于标识一个Map,Map的key为String类型,代表上传的键值对的key(与服务器接受的key对应),value即为RequestBody,有点类似 @Part的封装版本。
执行的代码:

File file = new File(Environment.getExternalStorageDirectory(), "messenger_01.png");
        RequestBody photo = RequestBody.create(MediaType.parse("image/png", file);
Map<String,RequestBody> photos = new HashMap<>();
photos.put("photos\"; filename=\"icon.png", photo);
photos.put("username",  RequestBody.create(null, "abc"));

Call<User> call = userBiz.registerUser(photos, RequestBody.create(null, "123"));

可以看到,可以在Map中put进一个或多个文件,键值对等,当然你也可以分开,单独的键值对也可以使用@Part,这里又看到设置文件的时候,相对应的key很奇怪,例如上例"photos\"; filename=\"icon.png",前面的photos就是与服务器对应的key,后面filename是服务器得到的文件名,ok,参数虽然奇怪,但是也可以动态的设置文件名,不太影响使用

3.8 下载文件

@GET("download")
Call<ResponseBody> downloadTest();

Call<ResponseBody> call = userBiz.downloadTest();
call.enqueue(new Callback<ResponseBody>()
{
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
    {
        InputStream is = response.body().byteStream();
        //save file
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t)
    {

    }
});
可以看到可以返回 ResponseBody,那么很多事都能干了。也看出这种方式下载感觉非常鸡肋,并且onReponse回调虽然在UI线程,但是你还是要处理io操作,也就是说你在这里还要另外开线程操作,或者你可以考虑同步的方式下载。

4. 配置OkHttpClient

这个需要简单提一下,很多时候,比如你使用retrofit需要统一的log管理,给每个请求添加统一的header等,这些都应该通过okhttpclient去操作,比如addInterceptor

例如:

OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor()//log,统一的header等
{
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException
    {
        return null;
    }
}).build();
或许你需要更多的配置,你可以单独写一个OkhttpClient的单例生成类,在这个里面完成你所需的所有的配置,然后将 OkhttpClient实例通过方法公布出来,设置给retrofit。

设置方式:

Retrofit retrofit = new Retrofit.Builder()
    .callFactory(OkHttpUtils.getClient())
    .build();
callFactory方法接受一个 okhttp3.Call.Factory对象, OkHttpClient即为一个实现类。


5.整体请求过程

前面我们已经看到 Retrofit为我们构造了一个OkHttpCall,实际上每一个OkHttpCall都对应于一个请求,它主要完成最基础的网络请求,而我们在接口的返回中看到的 Call 默认情况下就是OkHttpCall了,如果我们添加了自定义的callAdapter,那么它就会将OkHttp适配成我们需要的返回值,并返回给我们。

先看下call接口代码:

    public interface Call<T> extends Cloneable {   
        //同步发起请求   
        Response<T> execute throws IOException;   
        //异步发起请求,结果通过回调返回   
        void enqueue(Callback<T> callback);   
        boolean isExecuted;   
        void cancel;   
        boolean isCanceled;   
        Call<T> clone;   
        //返回原始请求   
        Request request;  
    }  
接下来执行repos其实就是一个OkHttpCall实例,execute就是要发起网络请求:

Call<List<Repo>> repos = service.listRepos("octocat");  
List<Repo> data = repos.execute; 

parseResponse主要完成了由okhttp3.Response向retrofit.Response的转换,同时也处理了对原始返回的解析:

    Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {   
        ResponseBody rawBody = rawResponse.body; //略掉一些代码   
        try {   
            //在这里完成了原始 Response 的解析,T 就是我们想要的结果,比如 GitHubService.listRepos 的 List<Repo> T body = serviceMethod.toResponse(catchingBody);   
            return Response.success(body, rawResponse);   
        } catch (RuntimeException e) {   
            // If the underlying source threw an exception, propagate that rather than indicating it was // a runtime exception. catchingBody.throwIfCaught;   
            throw e;          
        }   
    }  
处理结果时我想要接入 RxJava,让接口的返回结果改为 Observable:

    public interface GitHub {   
        @GET("/repos/{owner}/{repo}/contributors")   
        Observable<List<Contributor>> contributors( @Path("owner") String owner, @Path("repo") String repo);   
    }  

只需要提供一个 Adapter,将 OkHttpCall转换为Observable即可。Retrofit提供了相应的 Adapter(RxJavaCallAdapterFactory)我们只需要在构造 Retrofit时,添加它:

addCallAdapterFactory(RxJavaCallAdapterFactory.create)
我们来看看RxJavaCallAdapterFactory是如何工作的:
我们只需要实现 CallAdapter类来提供具体的适配逻辑,并实现相应的Factory,用来将当前的CallAdapter注册到Retrofit当中,并在Factory.get方法中根据类型来返回当前的CallAdapter即可。知道了这些,我们再来看RxJavaCallAdapterFactory:

public final class RxJavaCallAdapterFactory extends CallAdapter.Factory{  
        @Override  
        public CallAdapger<?> get(Type returnType,Annotation[] annotations,Retrofit retrofit){  
            //判断returnType是否为RxJava支持的类型  
            Class<?> rawType = getRawType(returnType);  
            String CanonicalName = RawType.getCanonicalName();  
            boolean isSingle = "rx.Single".equals(CanonicalName);  
            boolean isCompletable = "rx.Completable".equals(CanonicalName);  
            if(rawType != Observable.class && !isSingle && !isCompletable){  
                return null;  
            }  
            return Adapter;//"获取你需要的Adapter 返回"  
        }  
  
        static final class SimpleCallAdapter implements CallAdapter<Ovservable<?>>{  
            private final Type responseType;  
            private final Scheduler scheduler;  
  
            SimpleCallAdpter(Type responseType ,Scheduler scheduler){  
                this.responseType = responseType;  
                this.scheduler = scheduler;  
            }  
  
            @Override  
            public Type responseType(){  
                return responseType;  
            }  
              
            @Override  
            public <R> Observable<R> adapt(Call<R> call){  
                //在这里创建需要作为返回值的Observable实例,并持有call实例,所以在Observable.subscribe触发时,call.execute将会被调用  
                Observable<R> observable = Observable.create(new CallOnSubscribe<>(call)).lift(OperatorMapResponseToBodyOrError.<R>instance());  
                if(scheduler != null){  
                    return observable.subscribeOn(scheduler);  
                }  
                return observable;  
            }  
        }  
}  


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值