Retrofit2 完全解析 探索与okhttp之间的关系(一)

来源:互联网 发布:淘宝退货卖家没收到货 编辑:程序博客网 时间:2024/06/05 14:33

一、概述

之前写了个okhttputils的工具类,然后有很多同学询问这个工具类和retrofit什么区别,于是上了下官网,发现其底层对网络的访问默认也是基于okhttp,不过retrofit非常适合于restful url格式的请求,更多使用注解的方式提供功能。

既然这样,我们本篇博文首先研究其所提供的常用的用法:

  • 一般的get请求(如何通过注解携带参数,拼接url)

  • 一般的post请求(包含各种注解的使用)

  • 上传文件

  • 下载文件等

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

  • 如何自定义Converter.Factory

最后呢,因为其源码并不复杂,本文将对源码进行整体的介绍,即

  • retrofit 源码分析

ok,说这么多,既然需要restful url,我只能捡起我那个半桶水的spring mvc 搭建一个服务端的小例子~~

最后本文使用版本:

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

二、retrofit 用法示例

(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完成对象的转化。

ok,这里可以看到很神奇,我们通过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-gsonJackson: com.squareup.retrofit2:converter-jacksonMoshi: com.squareup.retrofit2:converter-moshiProtobuf: com.squareup.retrofit2:converter-protobufWire: com.squareup.retrofit2:converter-wireSimple XML: com.squareup.retrofit2:converter-simplexmlScalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

    当然也支持自定义,你可以选择自己写转化器完成数据的转化,这个后面将具体介绍。

  3. 既然call.enqueue是异步的访问数据,那么同步的访问方式为call.execute,这一点非常类似okhttp的API,实际上默认情况下内部也是通过okhttp3.Call实现。

那么,通过这么一个简单的例子,应该对retrofit已经有了一个直观的认识,下面看更多其支持的特性。

(2)动态的url访问@PATH

文章开头提过,retrofit非常适用于restful url的格式,那么例如下面这样的url:

//用于访问zhy的信息http://192.168.1.102:8080/springmvc_users/user/zhy//用于访问lmj的信息http://192.168.1.102:8080/springmvc_users/user/lmj

即通过不同的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)查询参数的设置@Query

看下面的url

http://baseurl/users?sortby=usernamehttp://baseurl/users?sortby=id

即一般的传参,我们可以通过@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即可。

对了,我能刚才学了@PATH,那么会不会有这样尝试的冲动,对于刚才的需求,我们这么写:

 @GET("users?sortby={sortby}") Call<List<User>> getUsersBySort(@Path("sortby") String sort);

乍一看别说好像有点感觉,哈,实际上运行是不支持的~估计是@ Path的定位就是用于url的路径而不是参数,对于参数还是选择通过@Query来设置。

(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执行相关代码

ok,可以看到其实就是使用@Body这个注解标识我们的参数对象即可,那么这里需要考虑一个问题,retrofit是如何将user对象转化为字符串呢?下文将详细解释~

下面对应okhttp,还有两种requestBody,一个是FormBody,一个是MultipartBody,前者以表单的方式传递简单的键值对,后者以POST表单的方式上传文件可以携带参数,retrofit也二者也有对应的注解,下面继续~

(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执行相关代码

ok,看起来也很简单,通过@POST指明url,添加FormUrlEncoded,然后通过@Field添加参数即可。

(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"));

ok,这里感觉略为麻烦~~不过还是蛮好理解~~多个@Part,每个Part指定一个RequestBody。

这里插个实验过程,其实我最初对于文件,也是尝试的@Part RequestBody,因为@Part("key"),然后传入一个代表文件的RequestBody,我觉得更加容易理解,后来发现试验无法成功,而且查了下issue,给出了一个很奇怪的解决方案,这里可以参考:https://github.com/square/retrofit/issues/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:上面这个方案经测试也是可以上传成功的。

最后看下多文件上传~

(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,参数虽然奇怪,但是也可以动态的设置文件名,不太影响使用~~

(8)下载文件

这个其实我觉得直接使用okhttp就好了,使用retrofit去做这个事情真的有点瞎用的感觉~~

增加一个方法:

@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,那么很多事都能干了~~

but,也看出这种方式下载感觉非常鸡肋,并且onReponse回调虽然在UI线程,但是你还是要处理io操作,也就是说你在这里还要另外开线程操作,或者你可以考虑同步的方式下载。

最后还是建议使用okhttp去下载,例如使用okhttputils.

有人可能会问,使用okhttp,和使用retrofit会不会造成新建多个OkHttpClient对象呢,其实是可设置的,参考下文。

ok,上面就是一些常用的方法,当然还涉及到一些没有介绍的注解,但是通过上面这么多方法的介绍,再多一二个注解的使用方式,相信大家能够解决。

三、配置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即为一个实现类。


由于微信单篇文章不可超过2W字该文章拆成了三篇~~见谅,连续看即可。



继续往下,接Retrofit2 完全解析 探索与okhttp之间的关系(二)

0 0